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
LT
- about Data::Peticom @hayajo
- ワンライナーでデータ処理 @john_7701
トークに関するメモ
Enjoy! テキスト解析 30min @__papix__
はじめての文書検索と分類 40min Takashi Yukawa (長岡技術科学大学 知識システム研究室)
類似文書検索
文書分類
事例(学生さん 3名の発表)
- Twitterのツイート本分からの位置情報抽出
- 大規模災害時に負傷者に関するツイートから位置情報をマップ等に表示したい
- ツイートに地名を含むものから地名を「都道府県」「市町村」... など4つに分類(抽出) => 緯度、経度に直す
- 省略される地名(ex 「大字」とか「〇〇郡」とか)で時間がかかる
- 怒りを含むメッセージかを判別
- 問い合わせメールに怒りを含んでいるものがあればそれを先に対応とか
- 価格.comのレビューで「怒っているアイコン」「怒っていないアイコン」のついたレビューを学習データに用いる
- 問い合わせメールを分類器にかける
- Wikipediaから概念ベース作成
今回 ust配信されてました @aokcub++
Niigata.pm のステッカーもらいました
Niigata.pm 決起集会がありました
Niigata.pm が正式にpm.orgに登録されたので、何かやろうということでLT大会と飲み会です。(また写真撮ってない...)
LTのメモ (初参加ではない方の自己紹介は割愛)
- mazhulin さん
- 質問たくさんしてくれました
- 僕的に丁寧な回答出来なかったのでなんかすみません
- id:jewel12 さん
- 居酒屋LTと初心者向けRubyわーくしょっぷをやります
- 新潟の Rubyist が集まるイベント 居酒屋LT
- 初心者向けわーくしょっぷ
- 6.23 14:30-17:00
- ギークハウス新潟
- といあわせは jewel_x12 さん
- hachioji.pm の id:uzulla さん
- IRCが嫌いなので yancha 作った(から使え) http://yairc.cfe.jp:3000/
- チャンネルじゃなくてタグ
- pocket.io
- 勉強会の次はプロダクトをつくるといいよ(コードでコミニュケーション)<- 重要!!!!!!
- YAPCのトーク申請
- LTじゃなかった
- IRCが嫌いなので yancha 作った(から使え) http://yairc.cfe.jp:3000/
- ishiduca
- psgiアプリのストリーミングパターン
- インストール済みのnode.js モジュールの READMEとコードを読むやつ
- RiceCurryNodle さん
- 長岡技大生 && これからPerl ということなので、寝る間を惜しんでコード書いて欲しい
- id:aokcub さん
- hayajyo さん
- iPad手書きプレゼン
- Fluentdはキュー
- terminalでじゃんけん?
- 隠れて面白ことしてる
- Takashi Yukawa さん
- neko_gata_s さん
- YAPCのトーク申請
- リアルタイム通知の概要
- むかし、XMLHttpRequest等によるクライアントからのリクエストとレスポンス
- リクエストとレスポンスは一続き
- 頻繁におこる通信のコスト
- リアルタイムではない
- さいきん、longpollingとかwebsocketとか
- push型
- 「リクエストとレスポンス」という関係じゃない(場合がある)
- 複雑な構造
- ユーザの個別は簡単じゃない
- neko_gata_s 式リアルタイム通信の実装
- YAPCで話したい(ので投票してね)
- むかし、XMLHttpRequest等によるクライアントからのリクエストとレスポンス
- 委譲について
- 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/ を開く
うまくいくとこんな感じ
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 );
オチ
質疑応答で「関数ではなくクラスで提供しては」と言う指摘があったので、実装方法考えます