foreachで連想配列に代入した後はcurrentが変わるのでwhileでeachするとハマる

へぇ、phpってこういう動きするんだーと思ったので。
普段while each使わないのでハマったことなかったのだけど、何かbugがあるなぁと思うコードを見てみたらforeachの後にwhile使ってるのが原因ってことがわかった。



bugをわかりにくくしていたのは以下のことがあったから。

  • foreachしてるでカーソルは最後まで進む
  • ただし、foreachの中で元の連想配列に代入していた場合にはカーソルが2つめになる

このせいでwhileが中途半端に成功してるのがbugに繋がった。

<?php
$hash = array(
    'a' => 1,
    'b' => 2,
    'c' => 3,
);

var_dump('current => ' . current($hash)); // string(20) "before: current => 1"

foreach ($hash as $key => $val) {
}

var_dump('foreach: current => ' . current($hash)); // string(19) "after: current => "

foreach ($hash as $key => $val) {
    $hash[$key] = $val;
}

var_dump('foreach + insert: current => ' . current($hash)); // string(19) "after: current => 2"

while (list($key, $val) = each($hash)) {
    var_dump($key, $val);
}

// string(1) "b"
// int(2)
// string(1) "c"
// int(3)

refs. PHP: each - Manual

USBに1Password.agilekeychainを入れててパスワード解除ができなくなったときの対処法

MacBook Airのディスク容量が少ないのでDropboxをUSBに入れてそこに1Passwordを入れているのですが、
USBを外して付け直したりすると1Passwordが上手く動かないことがあり困りました。

現象

  • 正しいマスターパスワードを入れているのに解除できない
    • コンソールにはこんな感じのエラーが出る

13/03/17 14:11:47.436 1Password Helper: 暗号化キーファイルは存在しますが 'file://localhost/Users/monmon/Dropbox/1Password/1Password.agilekeychain/data/default/1password.keys' からは読み込めません

  • 1passwordのhelper(メニューに出てるやつ)が起動したり終了したりを繰り返す

対処法

権限の問題だと思うんですがググったりしても同じような例が出なかったので、
1Passwordを終了させた後に

mv /Users/monmon/Library/Containers/com.agilebits.onepassword-osx-helper ~/tmp/. 

みたいにしてonepasswordの情報をよけて起動し直しました。

そうすると「初めてお使いですか?」「使ったことありますか?」のあの起動画面が出てくるので、「使ったことある」を選んで進めればよしと。

Rubyはハッシュに数値と文字列のkeyが持てるんですね

Rubyで書かれたコードにbugがあって調べることになり、初めて知りました。

Ruby

% ruby -e 'h = {1 => true, "1" => false}; p h'
{1=>true, "1"=>false}

Perl

% perl -MData::Dumper -e '$h = {1 => true, "1" => false}; warn Dumper $h'
$VAR1 = {
          '1' => 'false'
        };

PHP

% php -r '$h = array(1 => true, "1" => false); var_dump($h);'
array(1) {
  [1]=>
  bool(false)
}

追記

ということで、

  • Object#hash ハッシュの格納に用いられるハッシュ値の計算
  • Object#eql? キーの同一性判定

の2つを定義すれば何でもkeyになるよという話。
ハッシュの特性を考えれば確かにそうですよねそうですよね。

Perlとバーコードリーダーで本棚整理をするよ! #vgadvent2012


こんにちはこんにちは、VOYAGE GROUPエンジニアblog Advent Calendar 17日めを担当するmonmonといいます!
どんなネタにしようか土曜日まで迷ったあげく、せっかくの会社のAdvent Calendarなので会社に関係する話にしました!

どんな話をするの?


会社の1FにOASISという図書室がありまして、その本の整理をしたいなぁと思いました(あくまで今回のネタ用なので実際はやらないです)。
なので、今回はその話をしようと思います。
ちなみに上の展開写真がOASISね。

何でこの話にしたの?


理由は3つありまして、

という感じです。


仕事でもそうだけど、何かを作り出すときにこういう部品をつなぎ合わせることで形になっていくってのは楽しいものですね。
あぁ!そういえばジョブズも「役に立つ可能性があるとは思わなかった点と点が、10年後に繋がって素晴らしいものができた」みたいな良い話をしてましたね!それだ!

処理の流れ


さぁ!ということで処理の流れだ!最終的に以下のような感じになったよ!

  1. バーコードリーダーでバーコードを読み取り、ISBN13をqueueに入れる
  2. queueからISBN13を取り出しAmazon Product Advertising APIで検索
  3. あればさらにY!の日本語形態素解析API形態素解析
  4. それらをSQLiteに突っ込む
  5. あとは全文検索でうまー

これをひとつずつ見ていこう!

バーコードリーダーでバーコードを読み取る


バーコードリーダーはこれです。
USBで繋ぐと「ぴっ!」って鳴って、バーコードを読み込むと上の部分が緑に光り、ISBNを入力してくれます。便利!

ジョブキューを使う


ということで次。
ISBNを読み込んだ後、その検索結果を待たずに次のISBNを読み込みたいためジョブキューを使います。
で、今年のYAPC::AsiaでもQudoの話があったし、なのでQudoを使ってみようかなと調べたんですが、
調べているうちにこの辺の記事で今回の用途ならJonkでいいかもと思い変更しました。


Jonkの使い方は
https://metacpan.org/module/Jonk

https://metacpan.org/module/Jonk::Cookbook::Basic
に書いてあり、これをまるっとコピーしてきたらサクッと動くくらい楽で、ほぼそのままコピペです。ありがたいありがたい。
2年前のPerl Advent Calendarでも取り上げられているのでもっと難しいことやるときはこっちも参考にしようかなと思ってました(が、時間なくてここまでいかず)。


具体的なコードはこんな感じ。while 1で無限ループ作ってひたすらISBNをキューに突っ込むだけです。

#!perl
# script/jonk-client.pl
use strict;
use warnings;
use FindBin;
use DBI;
use Jonk::Client;

use lib "$FindBin::Bin/../lib";
use MyApp;

my $setting = MyApp->new(appdir => "$FindBin::Bin/../")->setting;

my $dbh = DBI->connect("dbi:SQLite:$setting->{jonk}->{db}",'','');
my $jonk = Jonk::Client->new($dbh);

while (1) {
    print 'input ISBN: ';
    next if <STDIN> !~ m/(\d+)/xms;

    my $isbn = $1;
    my $job_id = $jonk->enqueue('Book', $isbn);
    print "enqueued job_id: $job_id\n";
}


で、取り出して仕事する方はこんな感じ。実際のロジックはMyApp::Worker::Bookにあるので、そっちを呼び出しているだけですね。

#!perl
# script/jonk-worker.pl
use strict;
use warnings;
use FindBin;
use DBI;
use Jonk::Worker;

use lib "$FindBin::Bin/../lib";
use MyApp;
use MyApp::Worker::Book;

my $setting = MyApp->new(appdir => "$FindBin::Bin/../")->setting;

my $dbh = DBI->connect("dbi:SQLite:$setting->{jonk}->{db}",'','');
my $jonk = Jonk::Worker->new($dbh => {functions => [qw/Book/]});
my $worker = MyApp::Worker::Book->new(
    appdir => "$FindBin::Bin/../",
    setting => $setting,
);

while (1) {
    if (my $job = $jonk->dequeue) {
        warn "work job: $job->{arg}\n";
        $worker->work($job);
    } else {
        sleep(3); # wait for 3 sec.
    }
}


ここで呼び出されたWorkerが

の3つの処理を行います

Amazon Product Advertising APIを使う


これは超簡単。Amazonのサイトで自分のkeyとsecret、それにassociate_tagの3つを取得した後、
CPANにあるURI::Amazon::APAを使えば良いです。
弾さんのブログに使い方の記事もあるのでハマる所はないですね。
唯一のハマりどころは日本のURLである「http://ecs.amazonaws.jp/onca/xml」の方を使うということかしら。


コピペするだけで本の検索ができちゃうなんて素敵ですね!

形態素解析APIを使う


さて、つづいて形態素解析です。
これは何で必要になったかと言うと、FTSを使うものの、タイトルなどを一綴りのまま入れてしまうとヒットしないパターンがあるのでそれをどうにかしたかったから。
以下のブログも参考にしました。
http://blog.mwsoft.jp/article/34911345.html


なので、「MeCabを入れて…」と考えたんですが、「今回そんなにガッツリやる必要ないのでもっと楽なのがいいなぁ」と方向修正。
結果、Y!の日本語形態素解析APIです。
で、「これもCPANにあるんじゃないかなぁ」と思ったらやっぱりあったよ!
https://metacpan.org/module/WebService::YahooJapan::WebMA


ただしURLが古いので以下のように変更して使います。

$WebService::YahooJapan::WebMA::APIBase = 'http://jlp.yahooapis.jp/MAService/V1/parse';


具体的なコードはこんな感じ。ほぼSYNOPSISのままですが、word_listの要素が1つの場合に配列になっていなかったのでそこだけ対応しました。

    my $api = WebService::YahooJapan::WebMA->new(
        appid => $self->appid,
    );
     
    my $res = $api->parse(
        sentence => $sentence,
        ress => 'ma',
        response => 'surface,reading,base form'
    ) or croak $api->error;
    my @word_list = (ref $res->{ma_result}->{word_list} eq 'ARRAY')
        ? @{$res->{ma_result}->{word_list}}
        : ($res->{ma_result}->{word_list});
     
    $self->_to_string($self->_flatten(@word_list));

コレを使って、「タイトル」「作者」「出版社」の3つを形態素解析し、それらをspase区切りの文字にしてDBに突っ込むようにしてます。


具体例をあげるとPerl CPANモジュールガイドの場合には、
Perl CPAN もじゅーる モジュール ガイド がいど つみた 冨田 尚樹 なおき わーくす こーぽれーしょん ワークス コーポレーション」のような文字列がDBに突っ込まれるわけです。


ふむ!すごいですね!これで「perl」でも「cpan」でも「モジュール」でも「冨田」でも検索にかかるようになりました!それだけじゃなく「もじゅーる」みたいな読みでもかかるようになりました!そして、なんと「つみた」でもかかります!すごい!

SQLite FTSを使う


さてさて、最後にDBについて。
今回の本データをためるDBですが、今まで話に出て来た通りSQLiteのFTS4を使いました
(ファイルだから開発も楽だしね!)


FTSの使い方は簡単で、いつものcreate tableの文を以下のようにするだけです。

CREATE VIRTUAL TABLE book USING fts4(
    isbn,
    asin,
    title,
    author,
    manufacturer,
    words
);

「CREATE VIRTUAL TABLE」になっているところと、「USING fts4」になっているところが違う所で、他は特に変わりません。
ただ、UNIQUE INDEX利かななくなっちゃうため、アプリ側でユニーク確認をするようにしました。
(もしかしたら何かやり方あるのかもだけど調べきれず。Stack Overflowにはtriggerとかでどうにかする例があったけどそこまでの話ではなかったので深く見てないです)


SQLの発行方法は「match」というキーワードを使うだけ。
また、SQLite FTS3 and FTS4 Extensionsにあるように「where table名 match 'hogehoge'」というようにテーブル名を指定すればカラムをまたいで探してくれます。


具体的なqueryは以下のような感じ。

SELECT * FROM book WHERE book MATCH 'perl cpan';

これで全てのカラムでperlcpanに引っかかるデータを取り出せます。

DEMO的なやつ


以上でできあがった仕組みがこちらです。
https://github.com/monmon/vgadvent2012-app


それでは、写真で使っている様子を見て行きます。


1. まずjonk-client.plを起動するとISBNの入力待ちになります。

2. あとはひたすらバーコードリーダーでISBNを読みまくります。入力するとすぐに「早く次のISBNくれよぉ!」状態になるのでひたすら入れまくります。

3. で、一方違うターミナルでjson-worker.plを起動させておけば、以下のようにキューに入ったISBNを取り出し、一連の処理を始めます。

4. 結果、SQLiteに本データがたまるので、あとは全文検索で検索してうまーです。

5. 「タイトル」と「作者」の両方にあるような「単語」も検索できて便利ですね!!!!

まとめ


初めは「iPhoneでガンガンscanしてブクログに突っ込んで終わり」みたいなネタを考えたんですが、いざバーコードリーダーでやってみるとISBN読み込んでqueueに突っ込むだけだからよりガンガンscanできて楽しかったです。
(ちょうど大そうじの季節だったし、このネタにして良かった!)
まぁエラー処理とか全然考えてないし、既に「原作」と「漫画」のように2人の作者がいる場合にはauthorカラムに「ARRAY(0x7f8f6ac2fe30)」のように突っ込まれちゃうbugが判明したけど気にしません。
作ってみないとわからないことあるしね!リーン何ちゃらだ!


ということで、「コピペばっかりでも簡単にここまで作れるよ、でもコピペばっかりだと全然考慮が足りないから仕事ではちゃんと理解して物を作っていこう」という話でした!
明日は誕生日が終わったばかりの@tadasyさんです!今からでも間に合うのでみんなmention飛ばして祝ってみてください!

Rである母集団のdata.frameから除きたい集団data.frameがあって、その結果が欲しいとき

母集団がこんな感じで

population <- data.frame(id=c(1, 2, 3), data=c('a', 'b', 'c'))

  id data
1  1    a
2  2    b
3  3    c

除きたいのがこんな感じのとき

except <- data.frame(id=c(2), data=c('b'))

  id data
1  2    b

で、こんな感じで!とinを使えばいいです

population[!(population$id %in% except$id),,drop=FALSE]

  id data
1  1    a
3  3    c

覚えておいた方がいいのは「drop=FALSE」の部分で、これがないと1列(ベクトル)の時に結果の次元が落ちてしまう(idってのが欲しいのに1 3が返ってくる)

> population <- data.frame(id=c(1, 2, 3))
> except <- data.frame(id=c(2))
> population[!(population$id %in% except$id),]
[1] 1 3

1つめのカンマの前で「どういう行を出すか」、2つめのカラムの前で「どういう列を出すか」(今回の場合は何もないので全て)、その後にoptionだと思えば良い

Rubyで2つの日付を元にからその間の月を全部出す

パッとググった感じこんなんでよさそう

#!ruby
require 'date'

(Date.parse("2010-08-01")..Date.parse("2012-11-07")).each{|i|
    next if i.strftime('%d') != '01'
    puts i.strftime('%Y%m')
}

参考:rubyなら日付を連続10日分の日付作るのがアツ−!と言う間です - それマグで!

追記

何かeachの中でnextっておかしいね。returnの方がいいんだろうか

Rで日付を出す。月の連番を出す。

RでSQL作るときとかに今月から半年分くらいの月が欲しかったりするんだけど、そんなときはSys.Date()使えばいい。

> paste(format(seq(Sys.Date(), length.out=6, by="-1 month"), "%Y%m"), collapse=", ")
[1] "201210, 201209, 201208, 201207, 201206, 201205"
  1. 現在の日付を取って、
  2. そこから-1ヶ月ずつ6個作り、
  3. formatを%Y%mにして、
  4. ", "で連結して文字列化