MongoDBドキュメントのキー名に "." が入ってると怒られます。

JSONのつもりでいたけどそうじゃなかった

"use strict";

function Tags () {
    this.tags = {};
    return this;
}
(function (tp) {
    tp.reg = /#([^#\s\(\)\[\]]+)/g;
    tp.index = function () { return Object.keys(this.tags); };
    tp.map   = function () {
        for (var i = 0, len = arguments.length, _str; i < len; i++) {
            _str = arguments[i];
            if (typeof _str === 'string' && /#/.test(_str)) {
                var reg = this.reg;
                var str = _str.toLowerCase();
                var res, matching;
                while (res = reg.exec(str)) {
                    matching = res[1];
                    if (! this.tags[matching]) this.tags[matching] = 0;
                    this.tags[matching]++;
                }
            }
        }
        return this;
    };
    tp.get = function (tagname) {
        return this.tags[tagname];
    };
})(Tags.prototype);

var mongoose   = require('mongoose');
var mongos     = 'mongodb://localhost' + '/HogeHoge';
var HogeSchema = new mongoose.Schema({
    body: String
  , tags: {}                // このキー名で引っかかった
});

mongoose.connect( mongos );
console.log('[mongoose] MongoDB connected "%s"', mongos);

var Hoge = mongoose.model('Hoge', HogeSchema);

function test (body, i) {
    var tagClient = new Tags;
    var hoge      = new Hoge({body: body, tags: {}});

    tagClient.map(body).index().forEach(function (tagname) {
        hoge.tags[tagname] = tagClient.get(tagname);
    });

    hoge.save(function (err) {
        console.log('test -%d: "%s"', i, body);
        (err) ? console.error(err)
              : console.log(hoge);
    });
}

[
    'foo'
,   'foo.hoge'
,   '.hoge'
,   '#foo'
,   '#foo.hoge'
,   '#node.js'
].forEach(test);

結果

[mongoose] MongoDB connected "mongodb://localhost/HogeHoge"
test -4: "#foo.hoge"
[Error: key foo.hoge must not contain '.']
test -5: "#node.js"
[Error: key node.js must not contain '.']
test -3: "#foo"
{ body: '#foo', tags: { foo: 1 }, _id: 504bf47b987c4d32ed000004 }
test -2: ".hoge"
{ body: '.hoge', tags: {}, _id: 504bf47b987c4d32ed000003 }
test -1: "foo.hoge"
{ body: 'foo.hoge', tags: {}, _id: 504bf47b987c4d32ed000002 }
test -0: "foo"
{ body: 'foo', tags: {}, _id: 504bf47b987c4d32ed000001 }

Niigata.pm tech talk #2 がありました

teck talk #2 が開催されてから一週間が経ちました。
タイミング外してるとは言え、(発表しなかったとは言え&懇親会見送りとは言え)そのレポートです。
(内容に誤解があることがありますので、ツッコミよろしくお願いします)

概要はこちらから Niigata.pm tech talk #2 : ATND

今回のテーマは「データ処理」

データベース運用の話とかDB関係のモジュールの話が多いのかなと思っていましたが、全くなかったところが Niigata.pm なんでしょうか? しかし皆さんアルゴリズムの話がきちんとできるあたり日々勉強してるんだなと。
あと、neko_gata_s さんが単純ベイズ分類器をトークテーマに選んだ理由を話してるんですけど、すげーなとか。

Talk
  • 猫でもわかった気になれる!単純ベイズ分類器 40min @neko_gata_s
  • Enjoy! テキスト解析 30min @__papix__
  • はじめての文書検索と分類 40min Takashi Yukawa (長岡技術科学大学 知識システム研究室)
LT

トークに関するメモ

猫でもわかった気になれる!単純ベイズ分類器 40min @neko_gata_s 資料(PDF) レポート

  • ベイズ分類の概要 => 資料参照
  • 何故ベイズ分類器を選ぶのか? というテーマ
  • 単純ベイズ分類器は精密ではない
  • 計算式をそぎ落としているが、高い確率の近似値を出せる
  • リソースの限られた環境の中で現実的なエンジニアリング手法

Enjoy! テキスト解析 30min @__papix__

  • 全国行脚してる
  • 形態素解析エンジン KyTea (きゅーてぃー と読むらしい)推し
  • Text::KyTea Perlドライバ
  • デモ
    • KyTeaインストール
    • Text::KyTeaインストール
    • KyTeaでの解析デモ
  • Mecabなどより高速(らしい)
  • KyTeaのインストール時にprefixいじると Text::KyTeaのインストールはコケる
  • Acme::WriteNovel => ライトノベル自動生成

はじめての文書検索と分類 40min Takashi Yukawa (長岡技術科学大学 知識システム研究室)

類似文書検索
  • ex 「コンピュータ」で検索(問い合わせ)した際、「電子計算機」も検索対象としなくてはならない。
  • ex 「天ぷら」で検索した際、「天丼」も検索対象として欲しい
  • ベクトル空間モデル
    • 形態素解析 => 単語を切り分けて列に => ある文書に含まれている単語をカウントアップ
    • 対象文書と問い合わせ文書両方でベクトルを作る
    • 対象文書と問い合わせ文書のベクトルの角度差が小さいほど類似した文書とする
  • 概念ベース => ベクトル空間モデルの派生
    • 文書ではなく単語もとのベクトルで表す
    • ある文書の中で、お互いに近い場所に出てくる単語を数える
  • 形態素解析エンジン
  • また遊びに来て欲しい
文書分類

SVM(サポートベクターマシーン)

  • 二つに分ける => ex: メールを「スパム」か「スパムじゃないか」の2分類
  • 分類境界(ベクトル)を作る
  • 教師あり学習
  • Algoryithm::SVM CPANにある
事例(学生さん 3名の発表)
  • Twitterのツイート本分からの位置情報抽出
    • 大規模災害時に負傷者に関するツイートから位置情報をマップ等に表示したい
    • ツイートに地名を含むものから地名を「都道府県」「市町村」... など4つに分類(抽出) => 緯度、経度に直す
    • 省略される地名(ex 「大字」とか「〇〇郡」とか)で時間がかかる
  • 怒りを含むメッセージかを判別
    • 問い合わせメールに怒りを含んでいるものがあればそれを先に対応とか
    • 価格.comのレビューで「怒っているアイコン」「怒っていないアイコン」のついたレビューを学習データに用いる
    • 問い合わせメールを分類器にかける
  • Wikipediaから概念ベース作成
    • wikipediaのダンプデータを使った
    • wikipediaの記事名を「見出し語」に
    • 説明文を形態素解析した語彙を「属性語」に
    • ベクトル比較をして類似語辞書ができる => あいまい検索に
    • 主成分分析で30000程度の見出し語を100まで圧縮。見出し語 => 概念に


今回 ust配信されてました @aokcub++

Niigata.pm のステッカーもらいました

Niigata.pm 決起集会がありました

Niigata.pm 決起集会

Niigata.pm が正式にpm.orgに登録されたので、何かやろうということでLT大会と飲み会です。(また写真撮ってない...)

LTのメモ (初参加ではない方の自己紹介は割愛)

  • mazhulin さん
    • 質問たくさんしてくれました
    • 僕的に丁寧な回答出来なかったのでなんかすみません
  • john_____ さん
    • COBOLのコードをperlで書きなおしたとか
  • ishiduca
    • psgiアプリのストリーミングパターン
    • インストール済みのnode.js モジュールの READMEとコードを読むやつ
  • RiceCurryNodle さん
    • 長岡技大生 && これからPerl ということなので、寝る間を惜しんでコード書いて欲しい
  • hayajyo さん
    • iPad手書きプレゼン
    • Fluentdはキュー
    • terminalでじゃんけん?
    • 隠れて面白ことしてる
  • Takashi Yukawa さん
    • Perl3の頃から
    • Perlのガチガチじゃないオブジェクト指向とローレベルを意識させる所が好き
    • 最近は学生にPerlを教えるのに tohoho さんのサイト見て学べという方針らしい
      • このあたりをNiigatapmでフォローできるといいんだけど...
  • neko_gata_s さん
    • YAPCのトーク申請
    • リアルタイム通知の概要
      • むかし、XMLHttpRequest等によるクライアントからのリクエストとレスポンス
        • リクエストとレスポンスは一続き
        • 頻繁におこる通信のコスト
        • リアルタイムではない
      • さいきん、longpollingとかwebsocketとか
        • push型
        • 「リクエストとレスポンス」という関係じゃない(場合がある)
        • 複雑な構造
        • ユーザの個別は簡単じゃない
      • neko_gata_s 式リアルタイム通信の実装
        • YAPCで話したい(ので投票してね)
    • 委譲について
    • LTじゃなかった
  • バラエティに富んだ人たちが来たなー という印象

自分のLTネタ

psgiアプリのストリーミングパターン

既出のネタだけど、Dancer を使った場合のストリーミングでいい方法がわかんなかったのでいいかなと

use strict;
use warnings;
use Plack::Builder;

# 基本
my $app0 = sub {
    my $env = shift;
    [ 200
    , [ 'Content-Type' => 'text/plain' ]
    , [ "Hello PSGI\n" ]
    ];
};

# ストリーミングの基本的なやつ
my $app2 = sub {
    my $env = shift;
    sub {
        my $respond = shift;
        my $writer  = $respond->([ 200, ['Content-Type' => 'text/plain']]);
        for (1..3) {
            sleep 1;
            $writer->write("$_\n");
        }
        $writer->close();
    };
};

# sleepによるプロセスのブロッキングを回避するのに AnyEventを使うやつ
use AnyEvent;
my $app3 = sub {
    my $env = shift;
    unless ($env->{'psgi.streaming'}) {
        return [ 500,
                 ['Content-Type' => 'text/plain'],
                 [ "this app can not streaming" ]
        ];
    }

    sub {
        my $respond = shift;
        my $writer  = $respond->([ 200, ['Content-Type' => 'text/plain']]);
        my $c       = 1;
        my $t; $t   = AE::timer 1, 1, sub {
            if ($c > 3) {
                undef $t;
                $writer->close();
            }
            eval { # こうしないと broken pipe とかなる
                $writer->write("$c\n");
                $c++;
            }
        };
    };
};

# Dancer つかってみた
use Dancer;
get '/' => sub {
    my $data = "";
    send_file \$data,
        streaming => 1,
        callbacks => {
            override => sub {
                my($respond, $dancer_resp) = @_;
                my $writer = $respond->([ 200, ['Content-Type' => 'text/plain']]);
                for (1..3) {
                    sleep 1;
                    $writer->write("$_\n");
                }
        },
    };
};

builder {
    mount "/hello"  => $app0;
    mount "/sleep"  => $app2;
    mount "/timer"  => $app3;
    mount "/dancer" => dance;
};

Dancerでのstreamingの最適解あったら教えて欲しい

インストール済みのnode.js モジュールの READMEとコードを読むやつ

node.jsのコード書いてて、関数の使い方を調べるのに perldoc みたいなのあったらいいなという動機で

  • Perlのようなドキュメントから ReadMe.md があったら、読めるようにする
    • `nodedoc module_name`
  • (ReadMe.md がなくても)コードをよめるようにする
    • `nodedoc -m module_name`
  • インストール済みのモジュールの一覧を出す(`npm ls` 相当の事をやる)
    • `nodedoc -ls`
  • モジュールのパスを表示する
    • `nodedoc -l module_name`

ようなのを書いてお茶濁した。機能追加したらそれなりに使えるかもしれないので todo としては

  • 依存モジュールは未対応なので依存モジュールも扱えるようにする
  • "./lib" 以下の jsファイルのコードを読めるようにする
  • package.json とか gyp の内容を表示できるようにする
  • シンタックスハイライトを使えるようにする


「あっという間」というには短すぎた二時間でした


次回のテーマは「データ」です。

横断検索アプリのあーきてくちゃっぽいもの

横断検索(メタ検索)な psgiアプリを再制作していて、大分構成が変わったので比較のメモ

現物のコードは https://gist.github.com/2698475 Meta::Doujin::Search - WebSocket(Web::Hippie) + Dancer + Twiggy — Gist にある。まだα版もいいところだけど。

XMLHTTPRequest + LWP + Starman の構成(旧版)
  • クライアントがリクエストを投げる時点で、1つの検索条件に対して複数のリクエストを生成している。そのため、通信量のオーバーヘッドがひどい。
  • "仮に、" リクエストを複数生成するタイミングをアプリケーションサーバーで行うようにすると
    • クライアントとアプリケーションサーバー間の通信コストは減る
    • サービスからアプリケーションへのレスポンスは複数あってもそれをクライアントに返す際は一つにまとめなくてはならない が、仮にレスポンスが非常に遅いサービスがあると、そのレスポンスが返ってくるまで、他のレスポンスと1つにまとめるまで無駄に時間が掛かる
         (        user      )
                   |            * 検索条件リクエスト
       '        browser       '
       '           |          '
       '  ( XMLHttpRequest )  '  *
          |        |       |     * http通信 x3
          |        |       |     *
       '  serever  (Starman)  '  *
       '  |        |       |  '  *
       ' (     psgi.app     ) '  *  *
          |        |       |        *
          |        |       |        * http通信 based LWP x3
    'Service A'    |    'Service C' *
          |   'Service B'  |        *
          |        |       |        *
          |        |       |        *
       ' (     psgi.app     ) '  *  *
       '  |        |       |  '  *
       '  serever  (Starman)  '  * http通信 x3
          |        |       |     *
          |        |       |     *
       '  ( XMLHttpRequest )  '  *
       '  |        |       |  '
       '  |     browser    |  ' * DOM描画
          |        |       |
         (        user      )
  • ユーザーが検索条件をブラウザ(クライアント)へ入力
  • クライアントは検索サービス数のリクエストをXMLHttpRequestアプリケーションサーバーにリクエストを投げる
  • サーバーはクライアントからのリクエストを各サービスに対応したリクエストに直して、LWPで該当サービスにリクエストする
  • 該当サービスからのレスポンスを待つ
  • 該当サービスからレスポンスが来たら、クライアントに対応したレスポンスに加工して、レスポンスを返す
  • クライントはアプリケーションサーバーからのレスポンスを表示する
WebSocket + AnyEvent::HTTP + Twiggy の構成(新規)
         (        user      )
                   |            * 検索条件リクエスト
       '        browser       '
       '           |          '
       '      (webSocket)     ' *
                   |            * ws通信
                   |            *
       ' serever   (Twiggy) ' *
       '           |          ' *
       ' (     psgi.app     ) ' *  *
          |        |       |       * http通信 based AnyEvent::HTTP
          |        |       |       *     x3
     'Service A'   |   'Service C' *
          |   'Service B'  |       *
          |        |       |       *
          |        |       |       *
       ' (     psgi.app     ) ' *  *
       '  |        |       |  ' *
       ' serever   (Twiggy) ' * ws通信 x3
          |        |       |    *
          |        |       |    *
       ' (|   webSocket    |) '
       '  |        |       |  '
       '  |     browser    |  ' * DOM描画
          |        |       |
         (        user      )
  • ユーザーが検索条件をブラウザ(クライアント)
  • クライアントは検索条件を加工したリクエスト(1件)をwebSocket でアプリケーションサーバーにリクエストを投げる
  • サーバーはクライアントからのリクエストを各サービスに対応したリクエストをサービス数分生成して、AnyEvent::HTTPで該当サービスにリクエストする
  • 該当サービスからのレスポンスを待つ
  • 該当サービスからレスポンスが来たら、クライアントに対応したレスポンスに加工して、レスポンスをwebSocketで返す
  • クライントはアプリケーションサーバーからのレスポンスを表示する
現実のところ速くなりましたね。

node-ws-multi-proxy 触った

ので、メモ。
subtleGradient/node-ws-multi-proxy · GitHub

There are some WebSocket servers that only support a single connected client at a time.
You may want to connect multiple clients to that same server.
This lets you do that.

まだモジュールのコードは読んでないけど、簡単なUsage 載ってるので、まるっと github のコード丸写しで試してみた

proxy.js

var WSP = require('ws-multi-proxy').WebSocketProxy
//  , ws   = require('ws')
  , http = require('http')
  , fs   = require('fs')
  , html = './test.html'
  , port = 3210
;

var app = http.createServer(function (req,res) {
              fs.readFile(html, "utf-8", function (err, data) {
                  if (err) {
                      console.log(err);
                      res.writeHead(404);
                      res.end(err.toString());
                      return;
                  }
                  console.log(data.length);
                  res.writeHead(200, {'Content-Type' : 'text/html'});
                  res.end(data);
              });
          }).listen(port)
  , wssConfig = { server : app }
  , proxy     = new WSP({ webSocketServer : wssConfig })
;

proxy.mutateDataFromServer =
proxy.mutateDataFromClient = function (data, sender, receiver) {
    if (data.myCustomToken != receiver.myCustomToken)
        return false;

    data.value += " lol, augmented value";
    return data;
};

console.log('app start to listen on port' + port);

test.html

<!doctype html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
var wsUri = "ws://localhost:3210/proxy/ws://echo.websocket.org";
//"ws://echo.websocket.org/";
var output;
function init() {
    output = document.getElementById("output");
    testWebSocket();
}
function testWebSocket() {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
}
function onOpen(evt) {
    writeToScreen("CONNECTED");
    doSend("WebSocket rocks");
}
function onClose(evt) { writeToScreen("DISCONNECTED"); }
function onMessage(evt) {
    writeToScreen('<span style="color: blue;">RESPONSE: '
                + evt.data
                + '</span>'
    );
    websocket.close();
}
function onError(evt) {
    writeToScreen('<span style="color: red;">ERROR:</span> '
                + evt.data);
}
function doSend(message) {
    writeToScreen("SENT: " + message);
    websocket.send(message);
}
function writeToScreen(message) {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
}
window.addEventListener("load", init, false);
</script>
<h2>WebSocket Test</h2>
<div id="output"></div>
</html>

ターミナルから node proxy.js で、サーバーを立ち上げる
Chrome で http://127.0.0.1:3210/ を開く
うまくいくとこんな感じ
f:id:ishiduca:20120509170924p:plain

FileAPI とか WebSocket とか触りだしている

先週くらいからだけど、それらをゴニョゴニョするのに静的な HTML とか JS のファイルにコードを書いてる。
そういう時に静的なファイルを送るサーバーと、WebSocket通信できるサーバーが最低限必要になるし、その最低限をやるのに簡単なテンプレートなサーバー書いておいて、ゴニョゴニョする内容によっていじるとかしている。

server.js
/public/
    index.html
    /js/
        fuga.js
        hoge.js
    /css/
        index.css
    /img/
    /etc/

もっとシンプルなやり方ないかな? というのと、もっとシンプルにサーバー書けるようになりたいな と。

AnyEvent::HTTP::CookiesではなくAnyEvent::HTTP::Cookies::Unit の話をしてきました #niigatapm

Niigata.pm tech talk #1では AnyEvent::HTTPで使ったCookie情報 あるいは他のクライントで使ったCookie情報を相互に再利用できたらいいよね っていう話をしてきました。

      • -

外部WebAPIを利用してゴニョゴニョというのは、よくある(?)ケースだと思うけど、APIがないサイトは沢山あります。
その際、よく使われるのが ハンディブラウザ + スクレイピングツール で、WWW::Mechanize + Web::Scraper(Web::Query) の組み合わせが多いんじゃないでしょうか?

ただ、最近は AnyEvent 文脈で HTTP通信 を行うケースも多いと思います。
AnyEvent 文脈で WWW::Mechanize 相当の機能を持ったモジュールを僕は知りませんので、ログインなどセッション情報が必要なサイトではクッキーや他のヘッダー情報の管理は自前の処理機構上でやってます。

さて、比較的長い有効期限を持ったクッキーを使っているサイトもあります。その場合、実直にログインページからスタートする事なく、クッキーを使ってログイン状態のまま、目的のページへ移動することが可能な場合があります。
このCookie 情報をお気に入りのAnyEvent文脈、そうでない文脈で共有できると面白そうです。

Cookies情報の共有する際に
  • AnyEvent::HTTP は単なるハッシュリファレンスでCookie情報をプールしてAnyEvent::HTTP自体に情報を操作させるのに対し、WWW::Mechanize は HTTP::Cookiesオブジェクトの内部で情報をプールし操作は HTTP::Cookiesのメソッドを使う
  • 内部データの構造は似た構造
  • キー&バリューの構造が異なる。AnyEvent::HTTP では ハッシュリファレンス。HTTP::Cookies では 配列リファレンス
  • AnyEvent::HTTP は ネットスケープ準拠の Cookieに対応するのに対し、 HTTP::Cookies は Set-Cookies2 まで対応
  • 他のクライアントで利用できる保存形式が必要
「他のクライント」を考えてみると
  • WWW::Mechanize (isa LWP::UserAgent)
  • 実際のブラウザ
  • Ruby などの他の言語のクライアント

今回、HTTP::Cookies(::*) のsaveメソッドを使って保存する戦略を取ることにしました。2012.04.28現在
HTTP::Cookies の場合、デフォルトの保存形式は Set-Cookies3 で保存します。(サブクラスの場合は頃なる場合もあります。HTTP::Cookies::Safariでは plist形式のXML文書になります)


前置きが長くなりましたが本題

  • AnyEvent::HTTP が扱った Cookie 情報を HTTP::Cookies にセットする関数
  • (WWW::Mechanize, etc上で) HTTP::Cookies が扱った Cookie 情報を AnyEvent::HTTP で扱えるフォーマットに変換する関数
  • AnyEvent::HTTP が扱った Cookie 情報を HTTP::Cookies の 内部で扱うフォーマットに変換する関数(使いどころがないけど)

の関数を提供するモジュール Ginger::Cookies を書いてみました(α版。細かいところは見なおさないといけないと)。

デモコード(一部ふせております)

#!/usr/bin/env perl
use strict;
use warnings;
use HTTP::Cookies::Safari;
use Ginger::Cookies qw(set_cookies anyevent_style);
use AnyEvent;
use AnyEvent::HTTP;
use JSON;
use YAML;

my $WWW_PXXXX_NET = 'http://www.pxxxx.net';
my $stacc         = "${WWW_PXXXX_NET}/stacc/my/home/all/all";
my $file          = "$ENV{HOME}/Library/Cookies/Cookies.plist";
#my $file          = 'Cookies.plist';

my $cookie_jar = HTTP::Cookies::Safari->new;

$cookie_jar->load( $file );

my $jar = Ginger::Cookies::anyevent_style $cookie_jar;

my $cv = AE::cv;

http_request GET => $stacc,
    cookie_jar => $jar, sub {
        my($content, $hdr) = @_;
        if ($hdr->{Status} ne '200') {
            warn "failed: $hdr->{Status} $hdr->{Reason} $hdr->{URL}";
            return $cv->send;
        }
        my $tt    = ($content =~ /pxxxx\.context\.token\s=\s\'([^']+)\'/)[0];
        my $login = ($content =~ /pxxxx\.user\.loggedIn\s=\s([^;]+);/)[0];
        if ($login) {
            http_request GET => "${stacc}.json?tt=${tt}",
                cookie_jar => $jar, sub {
                my($content, $hdr) = @_;
                if ($hdr->{Status} ne '200') {
                    warn "failed: $hdr->{Status} $hdr->{Reason} $hdr->{URL}";
                    return $cv->send;
                }
                my $table = decode_json $content;
                warn Dump $table;

                $cv->send;
            };
        }
};

$cv->recv;

set_cookies $cookie_jar => $jar;

$cookie_jar->save( $file );


オチ
質疑応答で「関数ではなくクラスで提供しては」と言う指摘があったので、実装方法考えます