2015年12月30日水曜日

TOPPERSにトライ

自分は組み込み系のプログラムを作る仕事はほとんどしたことがないのですが、最近IoTが騒がれており、Raspberry Piもいじっているので小型のRTOS(リアルタイムOS)に興味がでてきました。
ただ、RTOSというのは通常特殊なメーカから販売されている高価なものしか使ってみたことがなく(しかも組み込み用途では使ったことありません)、オープンソースで手頃なものというとITRONになります。
TOPPERSというITRONの実装がかなり有名なようで、ぐぐると結構かかります。調べてみると、純粋なITRONの仕様から少し拡張されているようですが、活動が2015年現在でも活発に行われており、勉強用にLinux/Windows上でも動作できるエミュレータがついているとか。いきなりRaspberry Piにインストール、という前にちょっと勉強がてらLinux上で動かすのにトライしてみました。
(注:結論を先に述べておきますが、結局失敗しました。どうもLinux上のエミュレータの実装が、かなり古いLinux(おそらくkernel2.6系?)の時から更新されていないようで、今のUbuntu等のkernel3.x系で動かすのはかなり困難が伴うようです

1.TOPPERSのDL
TOPPERSのHP(http://www.toppers.jp/)に行ってみると、様々なバージョンがあるのにびっくりしました。各バージョンがどんな内容かひと通り読むだけで結構時間がかかりましたが、とりあえずスタンダードな以下をDLしてきました。

・TOPPERS/JSP-1.4.4.1

2.TOPPERSのビルド
以下のコマンドでLinuxエミュレータ上で動くものができるようです。(Linuxでは、sample1というプログラムが動くそうです)

$ ./configure -C linux
$ make depend
$ make

ところが当然ながら問題がでてきます。

(1)configureのperlスクリプトでエラーがでる。
configure:40lineで以下の行があります。

require "getopt.pl";

これ、perlのスクリプト起動時のオプションを拾ってくる機能拡張モジュールらしいんですが、現在では使われておらず、以下の様にするようです。

use Getopt::Std;
あと、Getoopt()→getopt()にする。

TOPPERSのMLの2012年にこの指摘があり、提案するとありましたがいまだに採用されていないようです。他の箇所にも同じような所がありましたが、修正してconfigureは通しました。

(2)make dependで、cfg/cfgがないと怒られる。
TOPレベルにcfg/というディレクトリがあるのですが、どうもここのビルドが実行されていないようです。make dependの段階でこれが必要ということは、configureの時点でアーキテクチャに応じてcfg/cfgがビルドされていることを期待しているんでしょうが、先の(1)の修正だけではまだ問題があったようです。
とりあえず手動でビルドして、cfg/cfgを作ります。
→これは、動かしたいTASKの登録をここでしておくので、先に設定をしてビルドしておくものだと判明。

(3)makeでエラー
さて、(2)までの修正でmake dependはパスしました。やっとビルドですが、以下の様なエラーがでてきました。

In file included from ../kernel/task.c:47:0:
../config/linux/cpu_context.h: In function ‘activate_context’:
../config/linux/cpu_context.h:74:35: error: ‘JB_PC’ undeclared (first use in this function)
     ((int *) &(tcb->tskctxb.env))[JB_PC] = (int) activate_r;
                                   ^
../config/linux/cpu_context.h:74:35: note: each undeclared identifier is reported only once for each function it appears in
../config/linux/cpu_context.h:75:35: error: ‘JB_SP’ undeclared (first use in this function)
     ((int *) &(tcb->tskctxb.env))[JB_SP] = (int)(((VB *) tcb->tinib->stk) +

何のことやらさっぱりですが、どうも自分の環境のライブラリには、"JB_PC", JB_SP"の定義がない、と怒られています。
そもそも、これらのdefineが何なのかぐぐってみると、cの関数で、setjmp()/longjmp()のための定義とのことでした。それらは本来以下の場所に定義されているようです。

/usr/include/setjmp.h
/usr/include/bits/setjmp.h

一つ目が2つ目をincludeしており、そこが肝心な部分なんですが、自分の環境(Ubuntu-14.04)ではそもそも2つ目のincludeが位置が違い、内容も大幅に変わっていて、JB_PC/JB_SPとかのdefineがありません。
bits/setjmp.hの定義場所が、CPUのアーキテクチャ毎のところになっていました。そもそも、このsetjmp()/longjmp()が何者なのかですが、c++等で例外処理にthrow→catchが使われていますが、cの時代にはこれが使われていたようです。つまり、ある関数を実行中(これが大事!)にエラーが発生したことを検知した場合、強制的に別のエラー処理用の関数にジャンプする関数だそうです。
かなりやばい系の関数ですが、当然その実装のためには様々なCPUのレジスタの退避とかしないと、エラー処理なんてできません。kernel2.x系の頃は、なんとかc言語だけで実装していたのを、kernel3.x系になったところでCPUアーキテクチャ別にしたようです。(性能とか、完全な動作を考えるとまともな考えですね。)
doc/linux.txtを見てみると、kernel2.2/glibc2.1の頃に作られた模様...、ちょっとこれはお手上げです。(・_・)


さてそうなるととりあえず自分ではお手上げの状態です。自分はc言語で、setjmp()/longjmp()を使ったことないので、既存のエミュレータをどう改修したらいいかわかりません。
自分のところにはまだ別の環境があるので、そちらで動かないか試してみます。


PS@20151231
自分の持っているVMPlayerの環境に、CentOS4と古いバージョンがありました。これで試してみると、

・(1)の問題は発生しません。やはりPerlの古い使い方のようです。
・makeで別の問題が発生しました。
 ../config/linux/tool_config.h:70: undefined reference to `software_term_hook'
ぐぐってみると、2007年に同じ問題にぶつかった人がMLに残していましたが、問題の箇所の関数はjsp-1.4.2のころは全部コメントアウトしてあったのが、jsp-1.4.3からコメントアウトが外されていて、エラーがでるようになったとのこと。別途、関数定義例があり、それを使ってビルド終了。

サンプルの、./jspが動くようになりました!ただコマンドをシリアルから入力することになっていますが、Linuxエミュレータ上で./jspプロセスのシリアルにどうやってつないだらいいのかわかりません。ttyで送ってみましたがダメでした。
まあ、とりあえず動かせる環境はできたんで、後はこれで勉強してみます。



2015年12月26日土曜日

gitによる開発に関して:特定部分を別branchで管理し、master等にコピーしたい

最近、悩んでいるのがgitの操作方法についてです。しかもやりたいことが少し特殊です。

・プログラムの開発をしているが、異なるbranchで開発しているもののうち、自分の担当している部分だけmasterに持ってきて(コピー)、全体の試験がしたい

通常ならば、branch間でmergeすればいいんですが、そうすると自分が使っているdevelop branchにある別のフォルダの内容がmasterと完全に別内容にしているため、それができない!
本来なら、リポジトリのディレクトリ設計の時に考慮しておくべきことなんですが、それに失敗した例です。ただ開発が最終局面にきていて、いまさらリポジトリの再設定をしてしまうと、これまでの各ファイルの改定履歴がリセットされてしまいます。(git等のバージョン管理システムではよくあることですが、ファイルのディレクトリの位置を変えただけでも、当該ファイルの履歴が消えてしまいます。せいぜい、当該ファイルが移動されたよ、というログが残る程度です。subversionは特別に履歴を引き継ぐオプションがあるようですが、特別扱いのようです。詳細は後の雑談で。)開発が結合試験に入っているため、ログが消えるのはかなり痛いです。

開発が終わってしまえば、ログが消えてもまだ我慢できますが、このフェーズではきついです。mergeできなくてもコピーだけでもできればいい・・・、最後はbranchを切り替えて、一旦別の場所にコピーしておいて、再度master branchに切り替え、(手動で)コピーしようかと考えました。色々とぐぐってみると、かなりトリッキーな技があるようですが、最終的に以下のコマンドで可能なことがわかりました。

$ git branch master
$ git checkout develop /home/hoge/git/repo/A/B
                    (branch名) (path名:フルパスで!)

これでdevelop branchのBディレクトリ以下のファイルがmaster branchのBディレクトリ以下にコピーされてきます。(master, developともrepo/以下のディレクトリ構造は基本的に同じはずですから、コピー元を規定するだけで問題ないはずです。万一、構造が変わっていたりすると・・・、ちょっと考えないと。)もちろん、modifyのstatusになりますので、commitとしとくなりしとかないといけません。(当然、ログには「developからコピーしたよ」等入れておきましょう)

注:上記checkoutではpath名をフルパスで規定しています。ぐぐったサンプルでは如何にもリポジトリのtopからのpath名と推測できるような書き方がしてあるものばかりでしたが、どうもフルパスが必要なようです。

具体的に例を示します。

1.gitリポジトリの作成

$ mkdir git/repo
$ cd git/repo
$ git init

2.リポジトリの中身を作る
今回は以下の様な階層を作ってみました。(詳細なコマンドは省略します)

A/ A.txt
     B/ B.txt
          D/ D.txt
     C/ C.txt

Aディレクトリのあるところに、".git"があります。各ディレクトリにあるテキストファイルには以下の様なテキストが入っています。(注:赤字の部分を別branchからコピーしてきます。ちなみに、B/とC/は同じA/以下の階層にいます。)

A.txtサンプルのテキスト
master branchのファイル

最初にファイル名を入れ、後はbranch名を入れてあります。(もちろん、この後commitしておきましょう)

3.develop branchの作成

$ git checkout -b develop

これで全く同じ内容でdevelop branchが作成され、develop branchに移動しました。

4.develop branch内のファイルの変更
次に、後にmaster brachの特定の部分(Bディレクトリ以下)にこのdevelop branchの内容をコピーするので、それがわかるようにファイルの中身を変更します。具体的には、以下の様にします。(これを、A, B, C, Dの4個のtxtファイルに行います)

A.txtサンプルのテキスト
develop branchのファイル

5.master branchへの移動、develop branchの一部をコピー

$ git checkout master
$ git checkout develop /home/hoge/git/repo/A/B

2回めのcheckoutはbranchを移動しません。単にdevelop branchのBディレクトリ以下をmaster branchにコピーしてくるだけです。これにより、B.txtと配下にいるD.txtがコピーされます。git statusをしてみると、この2つのファイルがmodifiedだといってきて、各ファイルの内容は以下の様になっています。

B.txtサンプルのテキスト
develop branchのファイル


これで自分のメインの開発はdevelop branchで行い、結合試験の必要に応じてmaster branchにコピーすることができます。


雑談:
今回、gitのことを色々調べていて思ったのが、こういうリポジトリ管理ではリポジトリ間でも「部分的な」コピーはどうやっても行えないということです。よく考えてみると、一つのリポジトリは一つの目的を持って作っているのであり、その一部だけを(履歴つきで)持ってくるのはかなり危ないことだと気づきました。そもそもその「履歴」(ログ)は別のリポジトリ(環境)で開発中のことが記録されているので、それを別のリポジトリで見ても何の意味もないか、逆に勘違いの元になる可能性があります。
同じように、一つのリポジトリ内でも、ファイルの移動を行うだけで「履歴」が消えてしまいますが、これはファイルの位置が変わるということは、当該ファイルの(開発)目的が変わる場合がほとんどで
、昔の履歴は害になることが多いはずです。
本来は、そんなことのないようにリポジトリを設計する段階できちんと決めておくべき(将来の可能性・拡張性を考えて)なのですが、世の中万能の人間ばかりではないので、filter-branch等駆使して、強引にログを書き換えてしまう参考例がいくつかありました。
ただ、最初から別リポジトリで開発を進め、途中から一つのリポジトリ管理下に置くようにすることは認められているようです。
後、調べている最中に"git-new-workdir"というツールの紹介がありました。このコマンドを使ってbranch間のコピー(共有?)を設定すると、どちらのbranchで操作を行っても、相手のbranchに反映される上に、履歴もつくのだとか。一瞬これこそ求めていたものだと思いましたが、よく考えたらこんなの使いはじめると「多用」しそうで危ないツールだと思いました。

2015年11月8日日曜日

pythonの超軽量webフレームワーク Bottle:Rapiroを制御する

さて本題ですが、webでRapiroを制御できるようにします。元々、これがしたくてpythonで動く簡単なフレームワークがないか探していました。

とりあえず、作ったweb画面を以下に示します。

何のひねりもありませんが、前進、後退、右旋回、左旋回と停止を指示できるようにします。Rapiroを制御するPythonプログラムではRapiroの運動制御を行っているArudinoに、制御用コンピュータとして搭載してあるRaspberry Piからシリアルで各コマンドを送るだけです。そのコマンドを、上記ページのボタンを押したら、送ってやるようにしてやればいいだけです。

Raspberry Piに以下の様なファイル(ディレクトリ構成)を作ります。

rapiro_study/
    - bottle.py
    - index.py
    views/
        - title.tpl

bottle.pyは、フレームワーク本体です。本来ならpythonのパッケージライブラリに入れるべきなんでしょうが、1ファイルと簡単な構成なのでわかりやすいようにここに一緒に置いています。index.pyがwebを制御する本体のプログラムです。(といっても、これだけです)views/以下に、htmlを置いておきます。(このディレクトリ名は固定のようです)今回は一つしか画面を用意していないので、一つしかありません。(title.tpl)これは拡張子が"tpl"ですが、内容は全く"html"です。

title.tpl
  1. <!DOCTYPE html>
  2. <html lang="ja">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>RAPIRO</title>
  6. </head>
  7. <body>
  8. <h1>RAPIRO Control page</h1>
  9.  
  10. <form name="frm" method="get" action="stop">
  11. <input type="button" onclick="document.frm.submit();" value="停止" style="font-size: 400%; width:200px; ">
  12. </form>
  13.  
  14.  
  15. <form name="frmF" method="post" action="forward">
  16. <input type="button" onclick="document.frmF.submit();" value="前進" style="font-size: 100%; width:100px; height:100px; position: absolute; left: 50%; top: 40%;">
  17. </form>
  18.  
  19. <form name="frmB" method="post" action="back">
  20. <input type="button" onclick="document.frmB.submit();" value="後退" style="font-size: 100%; width:100px; height:100px; position: absolute; left: 50%; top: 60%">
  21. </form>
  22.  
  23. <form name="frmR" method="post" action="right">
  24. <input type="button" onclick="document.frmR.submit();" value="右旋回" style="font-size: 100%; width:100px; height:100px; position: absolute; left: 70%; top: 50%">
  25. </form>
  26.  
  27. <form name="frmL" method="post" action="left">
  28. <input type="button" onclick="document.frmL.submit();" value="左旋回" style="font-size: 100%; width:100px; height:100px; position: absolute; left: 30%; top: 50%">
  29. </form>
  30. </body>
  31. </html>

index.py
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3.  
  4. from bottle import route, run, template, request
  5.  
  6. '''
  7. __debug__を追加。(通常実行時は、__debug__ = 1)
  8. Macでデバッグしたい時は、$ python -O rapiro3.pyで最適化をかけて実行する。
  9. '''
  10.  
  11. if __debug__:
  12. import serial
  13. com = serial.Serial('/dev/ttyAMA0', 57600, timeout=10)
  14.  
  15. # 出力を一カ所にまとめる。(デバッグ時には標準出力にだすようにする)
  16. def out(comStr):
  17. if __debug__:
  18. com.write(comStr)
  19. else:
  20. print comStr
  21.  
  22. # localhost:8000
  23. @route('/')
  24. def title():
  25. # views/title.tplを呼ぶ
  26. return template('title')
  27.  
  28.  
  29. # localhost:8000/show
  30. @route('/stop', method='GET')
  31. def stop():
  32. # GETパラメータの取得(username, men)
  33. command = request.forms.submit
  34.  
  35. print("in stop", command)
  36.  
  37. out('#M0') # 停止(初期姿勢)
  38.  
  39. # views/title.tplを呼ぶ
  40. return template('title')
  41.  
  42. # localhost:8000/forward
  43. @route('/forward', method='POST')
  44. def forward():
  45. print("in forward")
  46. out('#M1') # 前進
  47.  
  48. # views/title.tplを呼ぶ
  49. return template('title')
  50.  
  51. # localhost:8000/back
  52. @route('/back', method='POST')
  53. def back():
  54. print("in back")
  55. out('#M2') # 後退
  56.  
  57. # views/title.tplを呼ぶ
  58. return template('title')
  59.  
  60. # localhost:8000/right
  61. @route('/right', method='POST')
  62. def right():
  63. print("in right")
  64. out('#M4') # 右回り
  65.  
  66. # views/title.tplを呼ぶ
  67. return template('title')
  68.  
  69. # localhost:8000/left
  70. @route('/left', method='POST')
  71. def left():
  72. print("in left")
  73. out('#M3') # 左回り
  74.  
  75. # views/title.tplを呼ぶ
  76. return template('title')
  77.  
  78. # ビルドインサーバの実行
  79. run(host='rapiropi.local', port=8000, debug=True, reloader=True)
  80.  
これだけです。実行するときは、Rapiroのコンピュータ(rapiropiです)にログインし、以下のコマンドを実行します。

$ python index.py

その後、PCで以下のURLをブラウザで表示します。

http://rapiropi.local:8000/

URL名の最後が".local"とちょっと普通と違いますが、家庭内のPCを特にDNSを作らなくてもURLでアクセスできるようにパッケージを入れてあるからです。Webのボタンを押したら、本来なら別のページに飛ぶためのhtmlファイルを指定するところなんですが、今回は単にpythonに制御用の指示を出したいだけなので、単純に文字列を送信して、index.pyではそれを受けとって対応するシリアルコマンドを送ったら、また一つだけのホームページを表示するようにしています。ホームページの方は文字の大きさや何かを指定するために、CSSを使わず個別に指定しています。大したページでもなかったので、CSSを作るのが面倒くさかっただけです。

実際に動かしてみたところ、「ちょっと反応が鈍いかな?」という感じですがちゃんと動いてくれました。
制御用にGETとPOSTの両方を使っていますが、単に勉強用に両方を使ってみただけです。コマンドと関係はありません。(特に送信時に個別のデータはつけてませんし)JavaScriptの練習の時にはよくPOSTで送信していましたが、いざhtmlの定義だけでやるとき、最初は"submit"ボタンしか送信する方法が思いつかず悩みました。調べていたら、ボタンにformをつけて、そこで一つだけJavaScriptを使う方法(onclick="document.frm.submit();")が簡単なので、それを多用しています。実は、中々この方法がわからず、最後はJavaScriptを各ボタン毎に書かないといけないだろうかと思いながら、でもこれはhtmlじゃなくtplのbottle用のファイルだから動くんだろうかと悩んでいました。
結局、bottleを使いながらJavaScriptも使えるようですがちょっと特別なお作法があるようです。今回は処理的なところはPythonでやりたかったので、色々調べてみました。

2015年9月23日水曜日

pythonの超軽量webフレームワーク Bottle

pythonでおもちゃを動かすプログラムを作っていて、それをwebからコントロールしたくなりました。 何か楽な手段はないかと探したら、pythonによる超軽量なwebフレームワーク”Bottle”があることを知りました。特徴は、bottle.pyというファイル一つ使うだけでいいこと。

1.bottleのインストール

インストールというほどのこともありません。以下のコマンドでbottle.pyをDLしてくるだけです。

wget http://bottlepy.org/bottle.py

ただこのコマンドを実行してみたところ、githubに飛ばされて、そこからDLしてきました。


2.最初のサンプル

bottle.pyの中に各種モジュールが入っているので、必要なものをimportしたpythonプログラムを書いてやります。超シンプルなものを以下に示します。(hello_world.pyで作成)

  1. from bottle import route, run
  2.  
  3. @route('/hello')
  4. def hello():
  5. return "Hello World!"
  6.  
  7. run(host='localhost', port=8080, debug=True, reloader=True)

これで、以下を実行します。(bottle.pyは同じ階層に置いておきます)
 $ python hello_world.py 

次にブラウザで、http://localhost:8080/hello にアクセスすると、ブラウザに”Hello World!”の文字が表示されます。 最初に文字コードを指定してやれば、日本語ももちろんいけます。

 # -*- coding:utf-8 -*-

さて、次に考えないといけないのは、ページ上に入力欄やボタン、リンクに飛ばすことができるよう、htmlを別途用意して、それを表示させるにはどうしたらいいか?どうもテンプレート(.tpl)ファイルを用意してやればいいようです。そちらについては、また今度結果をまとめようと思います。

2015年7月24日金曜日

日本の海岸線データの加工

かなり国土地理院のデータを間引いて、なんとか100MBのデータを数MBまでにしましたが、ノートPCなんかで使うにはまだまだサイズが大きいです。思い切って1/100に間引いてみましたが、日本の海岸線が途切れてしまいます。
やはり少し頭を使って、元になる国土地理院の海岸線データの特徴を調べてみます。

1.海岸線データの固まり方
先にも言いましたが、元データは県別のxmlデータになっていて、行政界、島毎にタグが分かれています。それを緯度経度だけ抽出してくると以下ののようになります。
  1. 35.04924833 136.83906528
  2. 35.05145167 136.83815250
  3. 35.05175111 136.83802834
  4.  
  5. 35.05899417 136.87838278
  6. 35.05921944 136.87846166
  7. 35.05952000 136.87857805
  8. 35.05949694 136.87866195
  9. 35.05947139 136.87874555
  10. 35.05914306 136.87861805
  11. 35.05894917 136.87855139
  12. 35.05899417 136.87838278
  13.  
  14. 35.05730000 136.87774750
  15. 35.05771861 136.87788778

途中、2行の空白行がありますが、そこが行政界の区切り、あるいは島のデータの始まりを表します。上記サンプルを見ると、2つ目のデータの固まりのデータの始まりと終わり(6行目と13行目です)が同じ緯度経度になっています。つまりこれは島だということです。
前回のプログラムでは、以下に注意しました。

  • 行政界、島の区切りを示す空白行は残す。
  • 空白行を出力したところで間引きカウンタを0クリアして、空白行に続く先頭データは積極的に出力する。


しかし、特に島のデータに顕著にでましたが、あまり間引く数が大きいときはデータの固まりの最後(空白行の直前のデータ)も残してやらないと島が閉曲線になりません。

2,改良版プログラム
ということで、島の最後のデータも残してやるように改修しました。
  1. #!/usr/bin/python
  2. # coding: utf-8
  3.  
  4. import sys
  5.  
  6. if __name__ == "__main__":
  7. argv = sys.argv # コマンドライン引数を格納したリストの取得
  8. argc = len(argv) # 引数の個数
  9.  
  10. # print argv
  11. # print argc
  12.  
  13. fi = open(argv[1], 'r')
  14. fo = open(argv[2], 'w')
  15.  
  16. # n行に1回出力するようにする
  17. ct = 0
  18.  
  19. preLine = ""
  20.  
  21. for line in fi:
  22. if line.strip() == "": # 島等のデータとの境界である空白行は残すようにする
  23. if (preLine.strip() != ""): # 空白行の直前のデータは残す(島の閉曲線を守るため)
  24. fo.write(preLine)
  25. fo.write(line)
  26. ct = 0 # 島のデータは少ないので1行でも残す
  27. preLine = line
  28. continue
  29.  
  30. if ct == 0:
  31. fo.write(line)
  32.  
  33. ct = ct + 1
  34. preLine = line
  35.  
  36. if ct == 100: # 100行に1回出力する
  37. ct = 0
  38.  
  39. fi.close()
  40. fo.close()

さてこれで1/100に間引いてみましたが、どうもデータのサイズが思うように小さくなりません。

3.改良版の処理結果を見る
再度、処理結果を調べてみました。
  1. 35.08614861 136.89070723
  2. 35.08342250 136.86787278
  3. 35.05776194 136.84691778
  4. 35.06403806 136.84048584
  5.  
  6. 35.05175111 136.83802834
  7. 35.05175111 136.83802834
  8.  
  9. 35.05899417 136.87838278
  10. 35.05899417 136.87838278

確かに島のデータは残されるようになっていますが、思いの外国土地理院のデータには小島がしっかりはいっているようで、同じ緯度経度が2回連続する固まりが大量にありました。こいつらは改良版のプログラムではどんなに間引率を大きくしても残されるようにしてあるため、思ったようにデータのサイズが小さくならなかったようです。
でも実際日本地図を使うようなときはこんな小島、表示もされないのでデータの無駄です。

4.小島のデータを削除する
手作業で削除するかとも思ったんですが、すぐに諦めました。さすがにデータのサイズが小さくならない原因だけあって大量に存在しています。あきらめて、2行同じ緯度経度のデータが続く箇所はフィルタするようなプログラムを作りました。(行政界の他のところで偶然そういう場所がないか、という恐れはありますがまあ少しくらい抜けてもそれに気づくほど拡大して使うことはないでしょう。もし使う必要があるなら、それはもう手作業でなんとかしてやるしかありません)
  1. #!/usr/bin/python
  2. # coding: utf-8
  3.  
  4. import sys
  5.  
  6. if __name__ == "__main__":
  7. argv = sys.argv # コマンドライン引数を格納したリストの取得
  8. argc = len(argv) # 引数の個数
  9.  
  10. # print argv
  11. # print argc
  12.  
  13. fi = open(argv[1], 'r')
  14. fo = open(argv[2], 'w')
  15.  
  16. # n行に1回出力するようにする
  17. ct = 0
  18.  
  19. preLine = ""
  20.  
  21. for line in fi:
  22. if line.strip() == "": # 島等のデータとの境界である空白行は残すようにする
  23. if (preLine.strip() != ""): # 空白行の直前のデータは残す(島の閉曲線を守るため)
  24. fo.write(preLine)
  25. fo.write(line)
  26. ct = 0 # 島のデータは少ないので1行でも残す
  27. preLine = line
  28. continue
  29.  
  30. if preLine != line:
  31. fo.write(preLine)
  32. else:
  33. preLine = ""
  34. line = ""
  35. continue
  36.  
  37. preLine = line
  38.  
  39.  
  40. fi.close()
  41. fo.close()

かなり手抜きプログラムですが、これでサイズを1/100にし、かつ2行しかないような小島のデータを削除して、データサイズが1.4MBまで縮小できました。結果を以下に示します。

先日の海岸線とほとんど見た目は変わりません。よく見れば、小さな小島は消えているんですがね。




2015年7月21日火曜日

日本の海岸線データを手にいれる

レポートを書くとき、何かと使うことの多いのが地図データです。しかも緯度経度で海岸線や県境のデータがあるとベストです。(緯度経度で地点をマークして一緒にEXCELで散布図にしてやれば正確な表現ができます)
実は国土地理院では昔から公開していますが、数年前からやたら複雑な書式のxmlなので尻込みしていました。少し長い休みがあるので、重い腰をあげてデータ整理でもしてみましょう。

1.海岸線データを入手する
ぐぐれば簡単に見つかります。DLするとき何に使う?とか簡単なアンケートに答えないといけませんが、「趣味」の項目もありますから特に気張らずに。(ただ、ライセンス的にフリーというわけではないので注意はしてください。個人目的、研究用等に使うならご自由にという程度です)
データは県ごとに分かれています。

2.xmlフォーマット
各県ごとにデータが分かれています。しかも、県の中でも行政界ごとにxmlのタグが分けられているようです。以下に例を示します。

  1. <gml:LineStringSegment>
  2. <gml:posList>
  3. 35.55061306 140.11532166
  4. 35.55081833 140.11538084
  5. 35.55206806 140.11517666

xmlのタグ、<gml:posList>で識別されています。数字は一目で緯度経度とわかります。(国土地理院なので、今は当然世界測地系です)

3.海岸線データを取り出す
テキストエディタで取り出してもいいんですが、なにせ数があります。こちらは単に日本地図が描きたいだけなんですが、きちんと県別、さらに行政界毎にデータがタグで分離されているため、テキストエディタで手作業でやってたら死にます。
とりあえず、xmlなので、以下のプログラムでこのタグ部分だけ抽出してやります。

  1. # coding: utf-8
  2.  
  3. import xml.dom.minidom
  4.  
  5. if __name__ == "__main__":
  6.  
  7. dom = xml.dom.minidom.parse("sample.xml")
  8.  
  9. for line in dom.getElementsByTagName("gml:posList"):
  10. print line.firstChild.data

以下の様な感じで海岸線のデータだけ標準出力にでてきますので、それをファイルにリダイレクトしてやります。

35.55061306 140.11532166
35.55081833 140.11538084
35.55206806 140.11517666
35.55243944 140.11512389
35.55278833 140.11507416

4.データの間引き
さてこうしてできた海岸線のデータですが、単に数値の羅列ですがなんと100MB強あります!県毎にsheetを分ければなんとか日本の海岸線を散布図で描けますが、一つのファイルにまとめてしまうと、サイズが(行数が)大きくEXCELが扱えないといってきます。どうせそんな細かいところまで見ないので、データの間引きをします。そのために用意したプログラムを以下に示します。

  1. #!/usr/bin/python
  2. # coding: utf-8
  3.  
  4. import sys
  5.  
  6. if __name__ == "__main__":
  7. argv = sys.argv # コマンドライン引数を格納したリストの取得
  8. argc = len(argv) # 引数の個数
  9.  
  10. # print argv
  11. # print argc
  12.  
  13. fi = open(argv[1], 'r')
  14. fo = open(argv[2], 'w')
  15.  
  16. # n行に1回出力するようにする
  17. ct = 0
  18.  
  19. for line in fi:
  20. if line.strip() == "": # 島等のデータとの境界である空白行は残すようにする
  21. fo.write(line)
  22. ct = 0 # 島のデータは少ないので1行でも残す
  23. continue
  24.  
  25. if ct == 0:
  26. fo.write(line)
  27.  
  28. ct = ct + 1
  29.  
  30. if ct == 10: # 10行に1回出力する
  31. ct = 0
  32.  
  33. fi.close()
  34. fo.close()

今回は起動時に入力ファイル、出力ファイルを読むようにしました。あと間引く時、元のデータでは行政界毎、及び島毎に空行が入っていました。ここを無視して間引いいてしまうと、小さな島だとデータがなくなってしまいかねません。ちょっとだけ工夫しました。間引く数は実際に試して実験です。結局、10分の1(約10MB)にしないとまともにEXCELは動きませんでした。使うPC環境にもよりますが、もう少し間引いた方がいい気がします。またどうせかなりの縮尺で使うつもりなので、こだわって島のでデータを消さないようにしました。小さな島は消したいんですが、属性として地名は入っているんですが、それが島かどうかは人間が判断してやらないといけません。(GISデータは最後は人力で編集しないと、いいデータにはなりませんね(>_<))

5.完成
以下に実際に作成した日本(白)地図を示します。


使うときは例で示したように、別途示したい位置の緯度経度のデータを用意して、データ系列を追加してやります。注意が必要なのは、普通にやると追加したデータのグラフも散布図の折れ線になってしまいます。単にX-Yの座標だけの散布図にしたい場合が多いでしょうから、その時はデータ系列を追加してから、追加したデータを選択し、グラフの種類を変えてやります。(「複合グラフ」といいます)


2015年6月21日日曜日

g++における、getline()の戻り値について

cのときからテキストファイルを読み込むときによく使っていたのがgetline()でした。

ssize_t getline(char** , size_t , FILE* )

1行ずつ読込、戻り値に読み込んだ文字数が返ってくるため、ファイルの最後までいくと読めないので、文字数0が返ってきました。それを使って、最後まで読んだかをチェックするのが普通でした。

while ( getline(line, 256, fp) !=0 )

こんな感じです。c++になるとifstreamで同じ関数があり、確か最初(20年くらい前)は同じ仕様だったと思います。ずっとそう思っていました…

Macのxcodeで既存のlinuxで作成したプログラムをコンパイルしようとすると、どうしてもこのgetline()でエラーがでて、ずっと悩んでいましたが今日やっと理由がわかりました。c++でのgetline()の戻り値はifstream自身でした。
つまり、読込に成功したら"true"、最後までいき失敗したら"false"を返すようになってました。
でもこれまでlinuxの方では、0とかNULLで比較していても何もいってこなかったんで気付きませんでしたが、本当は以下のようにチェックすべきでした。

while ( fp.getline(line, 256))

linuxの方は昔のcの実装がboolがなくて、unsigned intでやれ、という指定だったためそれとの互換性を考えていたようです。しかしmacは厳密にチェックするようになったため、エラーにしているようです。さて、既存の遺産をどうするか注意しないと。


2015年6月13日土曜日

既存のgit管理されたCプログラムをeclipseで開発

久しぶりにちょっとした備忘録です。

1.概要
2.eclipseで操作する前の状態
3.eclipseで操作する状態
4.やった方が感じがいいこと

こんな感じで進めていきたいと思います。

1.概要
他のPC、或いは既にプロジェクトが少し進んでいてそれがgit管理されています。そこに参加した、或いは最初はソースコードを書いていただけで、実際のコンパイルをこれから始めるという状況です。
Linuxを想定しますが、eclipse-CDTだとC/C++プロジェクトを作成でき、しかも今はGit管理パッケージが最初から入っています。(数年前はGit管理パッケージが最初は入っていなくて、いちいちEGitを後から入れたりして大変でした)

話を簡単にするために以下の状況を想定します。

・ソースはgithub等のリポジトリにあるわけではなく、何故かローカルでPCに入れてあります。(分散リポジトリなんで、簡単に実現できます)
・~/ProgWork/work以下にgit管理されたソースが入っています。
・そこにはmain.cだけが入っています。
・当然、~/ProgWork/workには .git ディレクトリがあります。

つまり、このgit管理されたものをeclipseで開発を(gitを使いながら)継続したいというわけです。

2.eclipseで操作する前の状態
とりあえずデフォルトの状態として、eclipseの作業ディレクトリは、$HOME/workspaceとしておきます。最初はもちろん何もプロジェクトがなく、空っぽとします。(注:でも最新のeclipseだと、workspace/RemoteSystemTemFilesなんてディレクトリができていました。何でしょこれ?)

3.eclipseで操作する状態
やることは以下の通りです。

(1)eclipseでFileメニューから"Import"を選択する。
(2)選択Dialogが表示されます。その中に、"Git"があります。"Projects from Git"を選択してNextします。
(3)次のDialogで"Existing local repository"というのがでてきますので、これを選択します。(注:ちなみに通常は外部のgithub等からプロジェクトをとってきますから、もう一つの"Clone URI"を選択します。こっちのやり方は以前にも書きましたのでここでは省略します。)
(4)ここで、~/ProgWork/work/.gitを選択します。
(5)後はproject wizardに進みますので、そのまま作業を進めればeclipseで開発が続けれます。
(6)ただこれではプロジェクトを作っただけで、まだ肝心のソースファイルがプロジェクトに入っていません。Fileメニューで作ったプロジェクトにソースファイルを追加してやります。
(7)ただこれだけではまだ、このプロジェクトはGit管理できません。eclipseのTeamメニューでもいつものGit関係のコマンドがでてきません。Teamメニューから"Share project"をしてやる必要があります。

ところでこの状態でディレクトリの状態はどうなっているんでしょう?ここからは実は2通りあります。今回ブログを書くにあたって実験してたら、2通りの状態になってしまいました。どちらが正しい状態かはわかりませんが、どちらも動くようなのでたまたまなのでしょう。

"sample"というc/c++のプロジェクトを作りましたが、~/ProgWork/work/sampleというディレクトリが作成されました。ビルドしたバイナリはここにできます。
問題は、eclipseが管理する、".project"なんですが、これが~/ProgWork/work/sampleにできる場合と、eclipseのデフォルトの、~/workspace/sampleにできる場合の2通りにわかれました。
どちらの場合でも、結局元のソースは~/ProgWork/work/以下にあり、eclipseからは普通に操作できますし、gitでcommitもできます。
(実は、自分はここでworkspace/sample以下にコピーが作成されてしまい、eclipse独自のgit管理に入ってしまうかと予想していましたが、そんなことはありませんでした^^;)

4.やった方が感じがいいこと
さて、残る問題はeclipseによって勝手に作成される、~/ProgWork/work/sampleというディレクトリです。これがgit管理下には入っていませんから、よくgitから文句を言われます。
こんなときは、~/ProgWork/work/.gitignoreファイルを作り、そこに"sample/"と文字列を追加しておけば、gitはそれは自分の管轄外と思ってくれて無視してくれます。
ただ、さらにいうと今度は".gitignore"も管理に入れろとgitは文句をいってきます。これを.gitignoreに含めるかは、宗教の問題になってきます。会社などで同じ開発環境で作業しているなら、.gitiginore自体も共有すべきだというのが、通常の考え方なんだそうです。ただまあこれは好みの問題で、自分の様に個人で遊んでいる場合には.gitiginoreに自身も含めてしまっています。
(だって、"git status"で毎回なんか文句言われると気になるから)


2015年5月24日日曜日

テーブルデータのグラフ表示

さてCouchDBを使って、テーブルデータを扱うhtmlはできましたが、まだ表だけです。やはりここはグラフ表示できないと使い物になりません。そこでjCanvasを使ってグラフ表示できるようにしてみました。 まずデータの読込です。

ここで1つデータを追加します。
とまあ、順調にできました。
いつもならここでコードを参考に載せるところですが、機能的にまだあまりに不足している部分があるので、今回はパスします。こういう汎用的なものなら、以下の機能が欲しいところです。
・グラフを描画するcanvas要素のサイズに応じて描画。
・グラフ描画の際の上下左右のマージンをパラメータ化。
・あと、グラフの軸に目盛りが欲しい!
はっきりいって「とりあえず描画できるのを確認した」というレベルのコードしかまだできていません。( ;´Д`)まだ使い物になるかわかりませんが、少し様子をみて使い物になりそうなら、コードを作り込んでからまたブログに載せます。



2015年5月5日火曜日

CouchDB テーブルデータを表示/更新

さて単純なデータの表示/更新はできました。続いてテーブルデータです。例としては高度による静圧を取り上げます。とりあえず、出来た結果の表示例を以下に示します。(注:うっかり例には「標準大気圧テーブル」と書いてしまいましたが、正確には標準大気圧を元にした静圧のテーブルです。後で気づきました。(^_^;))


これの元になったCouchDBのドキュメントは以下です。

{ "_id": "StaticPressure", 
 "_rev": "16-482692fb61559cf3ad7224021b093d63", 
 "name": "StaticPressure", 
 "value": { "StaticPressure": [ { "Alt": 0, "P": 10332 }, { "Alt": 1, "P": 9167 }, { "Alt": 2, "P": 8106 } ] }, 
 "unit": "Alt[km],P[kgf/m2]" }

取り扱いたいデータは、"value"項にあるObjectのArrayです。(後にデータの追加を説明しますが、最初はこれもjson形式だと勘違いしていましたが、正確には違うんですね。そのためjsonのデータハンドリングが使えず、JavaScriptのObjectのデータとして取り扱います。つまりObjectのArrayがjsonの一つのデータとして格納されている形になっています)

「読込」ボタンで動くJacaScriptを以下に示します。


  1. var url = 'http://localhost:5984/environment/StaticPressure';
  2. var json;
  3. var json_part; // 静圧テーブルデータ部分(正確に言うと狭義のJSONとは違う)
  4. function exec() {
  5. // データの読み込み
  6. $.ajax({
  7. type: "GET",
  8. url: url,
  9. dataType:'json',
  10. }).done(function(data) {
  11. json = data;
  12. json_part = data.value;
  13. for (var i=0; i<json_part.StaticPressure.length; i++) {
  14. // 表(table)の最後の行に追加
  15. $('#table1 > tbody:last').append('<tr><td>'+ json_part.StaticPressure[i].Alt +'</td><td>'+ json_part.StaticPressure[i].P +'</td></tr>');
  16. }
  17. }).fail(function(xhr, textStatus, errorThrown){
  18. alert('error!!');
  19. console.log("NG:" + xhr.status);
  20. console.log("NG:" + textStatus.status);
  21. $('#msg').append("失敗しました");
  22. });
  23. }


#table1というidのtableタグがhtml側に用意してあり、そこに読み込んだ静圧テーブルのデータを1行づつ追加していくだけです。

次に「追加」を実装します。最初の例に既に「読込」の右横に2個の入力フィールドとともに準備してあります。ここに、「0.5」「1000」という 値を入れて「追加」してみます。
無事データが一行追加されました。「追加」ボタンの実装を以下に示します。
  1. function add() {
  2. var newAlt = parseFloat( $('#inputAlt').val() );
  3. var newPressure = parseFloat( $('#inputPressure').val() );
  4.  
  5. // 入力データを追加する(単純に検索する)
  6. for (var i=0; i<json_part.StaticPressure.length; i++) {
  7.  
  8. if (json_part.StaticPressure[i].Alt <= newAlt) {
  9. continue;
  10. }
  11.  
  12. var newdata = {};
  13. newdata.Alt = newAlt;
  14. newdata.P = newPressure;
  15.  
  16. // 表(table)のi行に追加
  17. $('#table1 tr').eq(i).after('<tr><td>'+ newdata.Alt +'</td><td>'+ newdata.P +'</td></tr>');
  18.  
  19. // 静圧テーブルデータにも追加
  20. // 第1引数:変更を加えるindex
  21. // 第2引数:第1引数から難行のObjectを削除するか指定
  22. // 第3引数:変更として追懐したいObject
  23. json_part.StaticPressure.splice(i, 0, newdata);
  24.  
  25. break;
  26. }

今回は注意点が2箇所あります。
①17行目でtableに値を追加しています。前回はappend( )を使いましたが、今回はafter( )を使いました。理由がわかりませんが、append( )だとテーブルの形が崩れたからです。(右横に1列追加される感じになってしまいました)この辺はもう少し勉強が必要です。
②23行目で実際のメモリ中のデータにも挿入しています。最初にも書きましたが、てっきりこれがjson形式のデータと思い込んでいたので、pushやなんか使おうとしたんですが、エラーばかり返されてきました。JavaScriptのObjectというデータ形式の中で、特にハンドリングしやすいようにフォーマットを決められたのがjsonとのことで、JavaScript内部のメモリでの持ち方はどちらも同じですが、使う関数が違います。splice( )と呼ばれるArrayに対する操作を行う関数を使っています。

ところで最初に「読込」時、テーブルデーを扱いやすくするため、json内部のvalueの部分だけjson_partという変数に代入してました。これまさしくポインタの代入で、「追加」の方でjsn_partにデータを追加したら、しっかしjsonの方にも反映されていました。(この辺りはjavaと同じで、変数は基本ポインタで値を保持しているようです)
後はこの新しいデータをCouchDBに戻してやるのと、せっかくテーブルデータなのでグラフが欲しいところですが、それはまた次回に。



2015年4月30日木曜日

CouchDB データを表示/更新するサンプル

ずいぶん前回から時間が空いてしまいました。余裕がなかったのもありますが、一番大きいのがこのBloggerでHTMLのソースを表示する方法が中々わからなかったのが一番大きい理由です。prettyprint使っても、どうしても内部のhtmlを本物のタグと解釈してしまうんです。結局そのままブログにペーストするのは諦めて、「偽の」HTMLに変換してからprettyprintすることにしました。(一手間増えるのと、ブログ編集中に確認がしづらいのでこの方法はどうしても嫌だったんですが、どうもこれしかなさそうです。あと変換ツールの問題なのか、シングルクオート、ダブルクオートの前にバックスラッシュが入ってしまいます。手作業で少しは削除しましたが、途中でめげました。) さて以下がサンプルで作成したHTMLの表示例です。
地球半径をCouchDBに登録したものです。「読み込み」ボタンでCouchDBからデータを読み込み表示し、「変更」でvalueに入力されている値をCouchDBに書き込みます。デバッグ用に、一番下に読み込んだり、書き込んだりするjson形式のデータを表示しています。
以下がそのサンプルです。(注:クオートの前にバックスラッシュが入ってしまっているのは無視してください。)

  1. <!DOCTYPE html>
  2. <html lang="ja">
  3.  <head>
  4.   <meta charset="UTF-8">
  5.   <title>HTML5</title>
  6.   <script src="jquery-1.11.2.min.js"></script>
  7.   <script src="stringify.js"></script>
  8.  </head>
  9.  <body>
  10.   <p>Hello Ajax, couchdb データ更新をします</p>
  11.   <input type="button" value="読み込み" onclick="readCouch('msg');">
  12.   <input type="button" value="変更" onclick="change()">
  13.   <!-- <TEXTAREA cols="40" rows="6" id="texts" wrap="on"></TEXTAREA> -->
  14.   <script>
  15.    var url;
  16.    url = 'http://localhost:5984/environment/Radius';
  17.    var json;
  18.    function readCouch(id) {
  19.     var target = document.getElementById(id);
  20.     $('#texts').text("これから読みます:");
  21.     $('#msg').append("これから読みます:</br>");
  22.     $.ajax({
  23.      type: "GET",
  24.      url: url,
  25.      dataType:'json',
  26.     }).done(function(data) {
  27.      // alert('読み込み');
  28.      json = data;
  29.      $(\'#id_text\').val(data._id);
  30.      $(\'#name_text\').val(data.name);
  31.      $(\'#value_text\').val(data.value.toString());
  32.      $(\'#unit_text\').val(data.unit);
  33.      var str = $.stringify(data);
  34.      $(\'#msg\').append(str);
  35.     }).fail(function(xhr, textStatus, errorThrown){
  36.      alert(\'error!!\');
  37.      console.log(\"NG:\" + xhr.status);
  38.      console.log(\"NG:\" + textStatus.status);
  39.      $(\'#msg\').append(\"失敗しました</br>\");
  40.     });
  41.     $(\'#msg\').empty();
  42.     $(\'#msg\').append(\"うまくいった?</br>\");
  43.    }
  44.    function change() {
  45.     json.value = parseFloat($(\'#value_text\').val());
  46.     var jsonStr = JSON.stringify(json);
  47.     $.ajax({
  48.      type: \"PUT\",
  49.      url: url,
  50.      data: jsonStr,
  51.      dataType:\'json\',
  52.     }).done(function(data) {
  53.      var str = $.stringify(data);
  54.      $(\'#msg\').append(str);
  55.     }).fail(function(xhr, textStatus, errorThrown){
  56.      alert(\'error!!\');
  57.      console.log(\"NG:\" + xhr.status);
  58.      console.log(\"NG:\" + textStatus.status);
  59.      $(\'#msg\').append(\"失敗しました</br>\");
  60.     });
  61.    }
  62.   </script>
  63.   <br>
  64.   <table border=\"0\">
  65.    <tr>
  66.     <td align=\"right\"><label>id : </label></td>
  67.     <td><input type=\"text\" id=\"id_text\"></td>
  68.    </tr>
  69.    <tr>
  70.     <td align=\"right\"><label>name : </label></td>
  71.     <td><input type=\"text\" id=\"name_text\"></td>
  72.    </tr>
  73.    <tr>
  74.     <td align=\"right\"><label>value : </label></td>
  75.     <td><input type=\"text\" id=\"value_text\"></td>
  76.    </tr>
  77.    <tr>
  78.     <td align=\"right\"><label>unit : </label></td>
  79.     <td><input type=\"text\" id=\"unit_text\"></td>
  80.    </tr>
  81.   </table>
  82.   <br>
  83.   <p id=\"msg\"></p>
  84.  </body>
  85. </html>
  86.  

これまでGUIでのデータ入出力画面をMotifやらViduslaStuioで組んできましたが、こっちの方がOSを選ばないので汎用性が高いです。(ただ2次元のグラフも表示したい、3次元のグラフィックが欲しい、とか言われるとHTML5のCanvsタグが必要になってくるので、現時点ではブラウザを選んでしまうのが悩ましいところです。)

追記:
ところで上記サンプルでは更新したいのは"value"だけなのに、ドキュメント全体のjsonデータをCouchDBに送信しています。これって無駄が多くない?と思い調べてみると、jsonStrを作るstringify( )で第2引数に特定のjsonデータだけを指定してやればjsonStrもそれだけが作れることがわかりました。
これはいい!と思って早速試してみたんですが、CouchDBのデータを管理するのがドキュメント単位で、さらにそれにrev番号まで付いています。つまり変更したい部分だけのjsonStrを送信してしまうと、残りの部分がなくなったドキュメントになってしまいます。ちょっとこれではうまくありません。あきらめるしかないようです。(あまりでかいドキュメントが必要になるようなデータベース構成にすると処理時間が問題になってきます)

2015年2月23日月曜日

Xcodeでgithubを利用

ネットでよく見る話題ですが、サクッといくこともあれば、苦労している記事もあります。自分の場合は苦労しました ^^;) どうもsshキーの登録を手動でしないといけないようなんですが、その辺が詳しく書いてあるものがありません。基本的にはコマンドラインで一回手動でやれば自動的にキーは記録してくれますが、ちょっとメモしておきます。(まあ、こんなのは昔のeclipseでもよくありました。少し前から全部GUIでできるようになったと思います。)

1.Xcodeでプロジェクトの作成
とりあえず何かプロジェクトを作成しましょう。あと重要なことですが、最初にローカルでGit管理するかという問い合わせがひっそりとあります。(デフォルトがオフです)これのチェックを入れておくことを忘れずに。(後からGit管理する方法もあるようですが、Xcodeのバージョンがあがるとどうなるか分からないので、面倒くさければ新規に作り直した方が早いと思います)

2.remoteリポジトリの登録
まずgithubに管理用のリポジトリを作成しておきます。(これ重要。いろんなツールを見てきましたが、新規に作成するリポジトリまで作成してくれる統合ツールは見た事ありません。おそらくgithubが新規リポジトリ作成のAPI(URL経由のね)は作ってないんでしょう。そうしないと、いたずらされかねませんしね。)
XcodeのSourceControlからConfigureで、remoteとしてgithubの先ほど作成したリポジトリを登録しておきます。

3.最初のcommitをローカルにする
とりあえず、ローカルのリポジトリにcommitしておきます。

4,remoteにpushする
ここが一番の難関です。Xcodeから行おうとすると、いかにもIDとパスワードを聞いてきて、うまくいきそうに見えますが失敗します。最初はプロジェクト以下の.gitにsshパスワードが入っていませんから。
ツールを使ってもいいんですが、最初だけは手動でgitコマンドでpushします。

$ git push -u origin master

このときgithubのユーザID、パスワードを聞かれますがここでsshキーが.git以下に登録されますから、あとはXcodeでも自由に操作できるようになります。

毎回思うことですが、gitって便利だけどどうしてもコマンドラインからの入力が避けれないな〜。(コマンドラインでbranchの操作とか、差分見ようと思うと地獄なんですが、どうして公式にはGUI作ってくれないのか。そのあたりはLinusさんの主義なんですかね。GUIあたりは周りに任して、自分は本質の部分だけに集中するとか。自分はコマンドライン、そんなに気にならないほうだからいいけど、若いもんは使ってくれないよ。emacsで編集、make、debugまでするのは会社ではもう自分だけじゃなかろうか。今時のは、統合開発ツールあるのが当たり前と思ってるし。自分としてはツールの裏で何動いているのかわからない方が怖いんだが。)

2015年2月22日日曜日

c++でcurlを使う:couchDBへのアクセス

c++でcouchDBにアクセスしようと思ったら、curlライブラリを組み込んでやりましょう。今回はとりあえず自分のメモ用にサンプル載せておきます。

  1. #include <string>
  2. #include <iostream>
  3. #include <curl/curl.h>
  4.  
  5. using namespace std;
  6.  
  7. size_t callbackWrite(char *ptr, size_t size, size_t nmemb, string *stream)
  8. {
  9. int dataLength = size * nmemb;
  10. stream->append(ptr, dataLength);
  11. return dataLength;
  12. }
  13.  
  14. int main()
  15. {
  16. CURL *curl;
  17. CURLcode ret;
  18.  
  19. curl = curl_easy_init();
  20. string chunk;
  21.  
  22. if (curl == NULL) {
  23. cerr << "curl_easy_init() failed" << endl;
  24. return 1;
  25. }
  26.  
  27. curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:5984/environment/");
  28. curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callbackWrite);
  29. curl_easy_setopt(curl, CURLOPT_WRITEDATA, &chunk);
  30. ret = curl_easy_perform(curl);
  31. curl_easy_cleanup(curl);
  32.  
  33. if (ret != CURLE_OK) {
  34. cerr << "curl_easy_perform() failed." << endl;
  35. return 1;
  36. }
  37.  
  38. cout << chunk << endl;
  39.  
  40. return 0;
  41. }

couchDBにenvironmentというデータベースを事前に作っておきます。これでうまくレスポンスが返ってきました。JSON形式で返ってきますので、後は前回のpicojsonでハンドリングするだけです。

2015年2月21日土曜日

JSONデータのハンドリング:c++

さてこれまでブラウザ上でJSONデータの扱いを調べてきましたが、これらは入力データの編集用と思って調べてきました。こっからが本題のc++によるJSONデータの扱いです。

もともと、本職はc++プログラムがほとんどなんで、いつも入出力のGUIに悩まされていました。それをCouchDBで楽したいというところで、色々調べていましたがだいたい目処がついてきたので、さてそれをc++プログラムでどうやってハンドリングしたらいいのか?というところまできました。

c++でJSONのパーサはいくつか見つかりましたが、picojsonがヘッダファイル一つというシンプルさでなかなか秀逸です。早速これを試してみました。

まず試験データとしてのJSONファイル(test3.json)です。

  1. {"message":"success",
  2. "number":[1, 2],
  3. "person":{"name":"hoge", "age":38},
  4. "follow":[
  5. {"name":"hoge", "age":38},
  6. {"name":"huga", "age":42}
  7. ],
  8. "returnCode":0}

わざと複雑な階層構造を持たせています。このファイル名を引数として動くサンプルが以下です。

  1. #include
  2. #include
  3. #include "picojson.h"
  4. int main(int argc, char** argv)
  5. {
  6. picojson::value v;
  7. std::ifstream infile;
  8. std::string inStr = "";
  9. infile.open(argv[1]);
  10. while (!infile.eof()) {
  11. std::string work = "";
  12. infile >> work;
  13. inStr += work;
  14. }
  15. infile.close();
  16. std::string err = parse(v, inStr);
  17. if (!err.empty()) {
  18. std::cerr << picojson::get_last_error() << std::endl;
  19. return 1;
  20. }
  21. // dump json object
  22. std::cout << "---- dump input ----" << std::endl;
  23. std::cout << v << std::endl;
  24. // accessors
  25. std::cout << "---- analyzing input ----" << std::endl;
  26. if (v.is()) {
  27. std::cout << "input is an object" << std::endl;
  28. //const picojson::object& o = v.get();
  29. picojson::object& o = v.get();
  30. for (picojson::object::const_iterator i = o.begin(); i != o.end(); ++i) {
  31. std::cout << i->first << " " << i->second << std::endl;
  32. }
  33. std::cout << std::endl;
  34. // 一番外側のobjectの取得
  35. std::string& s1 = o["message"].get();
  36. //picojson::array& s1 = o["number"].get();
  37. std::cout << s1 << std::endl;
  38. std::cout << std::endl;
  39. // 値の変更
  40. o["message"] = (picojson::value)std::string("fail");
  41. // データの追加
  42. o.insert(std::map::value_type("piyopiyo", 10.0));
  43. for (picojson::object::const_iterator i = o.begin(); i != o.end(); ++i) {
  44. std::cout << i->first << " " << i->second << std::endl;
  45. }
  46. std::cout << std::endl;
  47. // 内部のobject値の取得
  48. picojson::object& o1 = o["person"].get();
  49. std::cout << o1["name"].get() << " " << o1["age"].get() << std::endl;
  50. std::cout << std::endl;
  51. // array値の取得
  52. picojson::array& a1 = o["follow"].get();
  53. o1 = a1[1].get();
  54. std::cout << o1["name"].get() << " " << o1["age"].get() << std::endl;
  55. std::cout << std::endl;
  56. // serialize(JSON形式の文字列に戻す。JavaScriptのstringify()と同じ。)
  57. std::string str = picojson::value(o).serialize();
  58. std::cout << str << std::endl;
  59. }
  60. return 0;
  61. }

実行すると以下の様な出力をだしてきます。

  1. ---- dump input ----
  2. {"follow":[{"age":38,"name":"hoge"},{"age":42,"name":"huga"}],"message":"success","nu\
  3. mber":[1,2],"person":{"age":38,"name":"hoge"},"returnCode":0}
  4. ---- analyzing input ----
  5. input is an object
  6. follow [{"age":38,"name":"hoge"},{"age":42,"name":"huga"}]
  7. message "success"
  8. number [1,2]
  9. person {"age":38,"name":"hoge"}
  10. returnCode 0
  11.  
  12. success
  13.  
  14. follow [{"age":38,"name":"hoge"},{"age":42,"name":"huga"}]
  15. message "fail"
  16. number [1,2]
  17. person {"age":38,"name":"hoge"}
  18. piyopiyo 10
  19. returnCode 0
  20.  
  21. hoge 38
  22.  
  23. huga 42
  24.  
  25. {"follow":[{"age":38,"name":"hoge"},{"age":42,"name":"huga"}],"message":"fail","numbe\
  26. r":[1,2],"person":{"age":42,"name":"huga"},"piyopiyo":10,"returnCode":0}

うまく動いていますね。階層構造の扱いも、値の変更も思い通りになります。
ちょっと面倒くさいのが、いちいちpicojson::value形式で読みこんだ後、データの操作のためにpicojson::object形式にしないといけないこと。JavaScriptでも同じような考え方でしたが何でですかね?picojsonの実装はヘッダファイル一つなので、少し読んでみると、picojson::valueではvector形式で読み込んでいるようですが、すぐにmap形式にしてしまいます。確かにこれならアクセスするのが簡単で、処理速度も早いんですがデータ項目の順番がkeyでソートされてしまい順番が変わってしまいます。JSONとしては問題ないんですが、デバッグ時に少し悩みそうです。

後、このサンプルではparse()関数を使っていますが、picojson::valueは、>>を使って直接読み込むことができます。わざわざサンプル用に、stringでファイルを読み込み、parse()関数を使うように変更しました。こっちの方がエラーの扱いがわかりやすいからなんですがね。

picojsonなかなかいいんですが、一つ難点を言えば、c++のtemplate機能を使い倒しているため、エラーが出てくると、わけのわからないエラーメッセージを出してくるのがつらいです。c++の機能なんで、しかたないんですが。


2015年2月14日土曜日

JSON形式のハンドリング

couchdbとアクセスするためJSONを扱えるよう、JavaScript/jQueryによるJSONの処理方法を調べてます。

$.ajax()でGETすると、JSON形式でcouchdbからの応答をObjectの型式で読み込みます。データにアクセスするのは楽なのですが、デバッグ用に文字列で全体を表示しようと思うとすごいめんどくさい。調べてみると、stringify()という関数がよくでてきます。以下の様なスプクリプトをstringify.jsとか保存しておいてjQueryを拡張して使います。

  1. (function($) {
  2. $.extend({
  3. stringify: function stringify(obj) {
  4. var t = typeof (obj);
  5. if (t != "object" || obj === null) {
  6. // simple data type
  7. if (t == "string") {
  8. obj = '"' + obj + '"';
  9. }
  10. return String(obj);
  11. }
  12. else {
  13. var n, v, json = [];
  14. var arr = (obj && $.isArray(obj));
  15. for (n in obj) {
  16. v = obj[n];
  17. t = typeof(v);
  18. if (obj.hasOwnProperty(n)) {
  19. if (t == "string") {
  20. v = '"' + v + '"';
  21. }
  22. else if (t == "object" && v !== null) {
  23. v = jQuery.stringify(v);
  24. }
  25. json.push((arr ? "" : '"' + n + '":') + String(v));
  26. }
  27. }
  28. return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
  29. }
  30. }
  31. });
  32. })(jQuery);

皆んな困ってるようで、ググってると結構いろんな所ででてきます。今度は、$.ajax()でPUTしようとすると、送信データの形式がObjectではなくJSON形式の文字列で渡さないといけません。 これではjQueryにstringify()がなぜ実装されていないのか訳がわかりません。 さらにググると、JavaScript1.7からJSON.stringify()というのが実装されているのがわかり、納得しました。結局、ライブラリとして作ったファイルは一瞬で必要なくなったかの様に思えたのですが、話はこれで終わりませんでした。

ところがどっこい、IE8だとこのJSONというオブジェクトが実装されてません。ブラウザに実装されているJavaScriptのバージョンをチェックするスクリプトがあったので、調べてみると1.3です!(IEだと正確にはJScriptですが)会社関係だとシステムが対応してるのがまだIE8というところが多いと聞いています。(XPがダメになった時に、IE6からやっとあがったばっかりという所が多いと思います) 結局、当面はstringify.jsを使うようにしたほうがいいようです。
ちなみに以下に、そのスクリプトを示します。


<SCRIPT Language="JavaScript">
// Java Script 1.0以降で動作する関数
function funcGetVer() { return "1.0" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.1">
// Java Script 1.1以降で動作する関数
function funcGetVer() { return "1.1" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.2">
// Java Script 1.2以降で動作する関数
function funcGetVer() { return "1.2" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.3">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "1.3" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.4">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "1.4" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.5">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "1.5" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.6">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "1.6" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.7">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "1.7" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.8">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "1.8" ; }
</SCRIPT>

<SCRIPT Language="JavaScript1.9">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "1.9" ; }
</SCRIPT>

<SCRIPT Language="JavaScript2.0">
// Java Script 1.3以降で動作する関数
function funcGetVer() { return "2.0" ; }
</SCRIPT>

<SCRIPT Language="JavaScript">
// Java Script のバージョンを取得して表示する関数
  function funcVerDisp()
{
var strVersion = funcGetVer() ;
alert( 'このブラウザーの Java Scriptのバージョンは\n\t' + strVersion + ' です。\n' ) ;
}
</SCRIPT>

なんか同じ関数を何度も定義してますが、SCRIPTでのlanguage指定がキモです。ここで対応するバージョンを指定していますが、同じ関数定義をすると最後のものが有効になります。そこでhtml内のbuttonからonclickでfuncVerDisp()をCallしてやるようにすれば、バージョン名をダイアログで表示してくれます。ちなみに私が普段使いしているchromeは1.7でした。(最初にこのテクニックを紹介していたサイトでは、SCRIPTの中で、Type="javascript"というtype属性をつけていましたが、それではうまく動きませんでした。調べてみるとtype属性はスクリプトバージョンには関係しないので、返ってそれ以降の指定が無効になってしまうようです。)

結局、stringify()の使い方ですが、以下のように使います。

  1. var jsonStr = '{"message":"success", "returnCode":0}';
  2. json = $.parseJSON(jsonStr);
  3.  
  4. var str = $.stringify(json);

最初にJSON形式の文字列をjQueryの関数でObject形式にしていますが、JavaScript1.7だと、JSON.parse()という同様のものがあります。しかしこれもIE8では使えないので、色んな環境で動かしたいならjQueryにまかせた方がいいようです。

2015年1月31日土曜日

CouchDB JavaScriptからの読み込みについて

CouchDBのUIを作ってみたくなり、htmlを書いてみます。せっかくなのでjQueryを使って、Ajaxを試して見たいと思ったのですが、ここからが苦難の始まりでした。

簡単なサンプルを書いてみたんですが、どうしてもNo 'Access-Control-Allow-Origin' header is present on the requested resource.というエラーがコンソールに出てきて失敗します。(ChromeのJavaScriptコンソールで確認しています)色々、調べてみたんですが、エラーの意味はXSSを防ぐため、ドメインが異なるJavaScriptを実行したり、読み込もうとするとでるエラーのようです。(数年前、これおを悪用したサイトの改竄が流行った記憶があります)しかし自分はローカル環境、あるいは家庭内の別PCで動かしているcouchdbから読もうとしているんですが、このエラーから逃げれません。
そこで、jQueryの問題なのかcouchdbの問題なのか区別するため、素に戻って単純にJavaScriptでローカルPCに置いたjsonデータを読むプログラムを書いてみました。

  1. function loadJson(id) {
  2. $('#msg').append("これから読みます:");
  3.  
  4. var url = "http://localhost/~hogehoge/test1.json";
  5. var xmlHttp;
  6.  
  7. xmlHttp = new XMLHttpRequest();
  8. xmlHttp.open("GET", url, false);
  9. xmlHttp.send(null);
  10.  
  11. $('#msg').append(xmlHttp.responseText);
  12.  
  13. $('#msg').append("うまくいった?");
  14.  
  15. }

(これをやるために前回「ウェブ共有」の設定を調べました)
ところがMac上のchromeからでは相変わらず同じエラーが出ます。幾ら何でもこれはおかしいだろうと、調べてみるとchromeではセキュリティを厳しくしていて、ローカルPC上のファイルをhttp経由で読もうが、同じチェックをしていてエラーを出すそうです。ちなみに同じものをSafariで実行してみたら、あっさり動きました。(ちなみに同様のスクリプトをjQueryで書くと、Safariでもエラーがでます。どうもjQuery内部で同様のチェックをしているようで、簡単に自分のとこだけで試してみようと思っているのに難儀なことです)

さてCouchDBのUIを作ろうと思ったらなんとかcouchdbの設定で'Access-Control-Allow-Origin' のヘッダをつけるようにしないといけません。
現状、使用しているcouchdbは1.6ですが、1.4のドキュメントには以下の設定があるのがわかりました。

local.iniに以下の設定を追加。

  1. [httpd]
  2. enable_cors = true
  3.  
  4. [cors]
  5. origins = *

特に[cors]の項目はデフォルトの設定ファイルには存在しておらず大丈夫かと思いましたが、うまくいきました。(ただこの設定は「全て許可する」という意味なんで、あまり推奨できませんが…本当は単にローカルPC上だけで動かしたいだけなら、origins=http://localhostとかするのがいいと思います。でもこれだと、http://127.0.0.1でアクセスすると拒否されてしまい、難しいところです。)
さて実際にcouchDBからデータを読み込んでみます。以下のプログラムで試してみました。(htmlにはボタンをつけて、以下のスクリプトを実行するようにしています)

  1. function readCouch(id) {
  2. $('#msg').append("これから読みます:");
  3. var url;
  4. url = 'http://localhost:5984/kakeibo';
  5. $.ajax({
  6. type: "GET",
  7. url: url,
  8. dataType:'json',
  9. }).done(function(data) {
  10. alert('読み込み');
  11. var str = parseJson(data);
  12. $('#msg').append(str);
  13. }).fail(function(xhr, textStatus, errorThrown){
  14. alert('error!!');
  15. console.log("NG:" + xhr.status);
  16. console.log("NG:" + textStatus.status);
  17. $('#msg').append("失敗しました");
  18. });
  19.  
  20. $('#msg').append("うまくいった?");
  21. }
  22.  
  23. function parseJson(json) {
  24. var ret = 'db_name : ' + json.db_name + '';
  25. ret = ret + 'doc_count : ' + json.doc_count;
  26. return ret;
  27. }

chromeで実行すると以下のようになります。(html上に、<p id=msg>が付けてあり、そこに追記するようにしています)


うまく読めましたね。

2015年1月23日金曜日

Mac Yosemiteでウェブ公開をする(apache2を動かす)

Lionから「設定」に「ウェブ共有」がなくなったそうです。まあ、昔からあまりに簡単にhttpdを起動できるので危ないな〜とは思ってましたが、いざなくなると困ります。Yosemiteではapacheを手動スタートしないといけません。(Lionsは試してなくて、Mavericksは使いましたが、そのときは試してませんでした。いきなりYosemiteに来たんで、気づかなかった)

$ sudo apachectl start

(passwdが聞かれます)

これで、http://localhost/をブラウザで開けるようになります。
この時のhomeは、/Library/WebServer/Documents/になります。(システムルートです)ただこれでは作業上面倒くさいので、従来の自分のhomeのSitesをホームにします。(この後設定ファイルを色々アンコメントしますが、デフォルトでそうなのか、あるいは古いバージョンからYosemiteにUpしたせいなのか正確なところはわかりません。自分はUpしてますので、設定しようと思ったファイルがすでにあるものの内容に問題があり、以下の通りに修正したので所々これでいいのか悩みました)

まず/etc/apache2/httpd.confでmod_userdirを有効にします。

$ sudo vi /etc/apache2/httpd.conf

mod_userdir.soをロードする行がコメントアウトされているので、アンコメントする。
(166行目付近の以下をアンコメントする。)

LoadModule userdir_module libexec/apache2/mod_userdir.so

# User home directories
Include /private/etc/apache2/extra/httpd-userdir.conf

(他のHPみると、phpのモジュールとかもコメントアウトされてしまうとか。そのあたりは必要に応じてということになりますが、基本安全側にコメントされてしまうようです。)

次にユーザーディレクトリを作成します。(これは昔のバージョンのものが自分は残っていました)

$ mkdir ~/Sites

ルート権限で、/etc/apache2/users/ユーザ名.confというファイルを作る。(書式がYosemiteのapache2.4になってから変わってます。以前は2.2だったそうで、サイトによって内容が若干変わってたりします。ただ少なくとも昔のままではダメなことは確かです。)

<Directory “/Users/ユーザ名/Sites/”>
DirectoryIndex index.html index.php
AllowOverride All
Options all
Require all granted
</Directory>

以下のファイルを修正し、アンコメントする。
$ sudo vi /etc/apache2/extra/httpd-userdir.conf

Include /private/etc/apache2/users/*.conf

これでapache2を再スタートすれば、http://localhost/~ユーザ名 で「ウェブ共有」できるようになります。(いくつかのHPでは、ユーザ名の頭の「~」が記されなくてうまくいかず、Yosemiteから変わったんだろうかと悩みましたが結局ここは昔のままのURLです。)
一度起動すれば、以降はマシンを再起動しても自動的に起動してくれます。


なんで急に「ウェブ公開」なんてやろうと思ったかというと、couchDBのUIをhtmlで作ってみたくなり、jQueryとかいじり始めたらどうもうまくいかず、基本のurlでのアクセスから試してみないとわからない、ということになったんです。ただ、ここからまた別の苦闘が始まるのですが、それはまた今度に。

2015年1月12日月曜日

CouchDB Replicatorについて

もう少しCouchDBの基本的な動作を確認します。今度はReplicator(複写)についてです。 作成したDBのBackUpや、あるいは複製を取る機能としてFutonにはReplicatorの機能があります。localにBackUpを保存してもいいんですが、折角なので別のPCにもCouchDBを設定して試してみます。(ちょっと別に興味があったので、MacBookAirにHomebrew入れてCouchDBをインストールしてみました) 

Replicatorの画面は以下の様な感じです。ここで簡単にkakeiboをMacBookAirに新規インストールしたCouchDBにコピーを作ります。(なお、先にMacBookAir側で内容は空でいいんですがコピー先のDBを作成しておく必要があります。どうもいきなり作成はしてくれないようです。PS:remoteから取ってくる場合は新規に作成するかと聞かれて、OKすれば自動的に作成してくれるようです。)


うまくいくとEvent欄にsessionの動作ログが表示されます。

さてここで実験です。localとMacBookAir側でわざとDBを異なる状態にしてReplicatorを動かしたらどうなるかを見てみます。まず、それぞれを以下の状態にします。(赤字がそれぞれ追加したドキュメントです)

local側

date    category      item       price
5/10     果物           バナナ       300
5/10       魚               鯛         1000
5/11     果物           みかん       300
5/11     果物           バナナ       300
5/12       魚               鯛           900
5/14       魚            まぐろ       1000


MacBookAir側

date    category      item       price
5/10     果物           バナナ       300
5/10       魚               鯛         1000
5/11     果物           みかん       300
5/11     果物           バナナ       300
5/12       魚               鯛           900
5/13     果物           みかん       600

相互にReplicateしてみると、それぞれ追加したドキュメントが相手に入ってくれます。


Replicate後

date    category      item       price
5/10     果物           バナナ       300
5/10       魚               鯛         1000
5/11     果物           みかん       300
5/11     果物           バナナ       300
5/12       魚               鯛           900
5/13     果物           みかん       600
5/14       魚            まぐろ       1000

その後、local側で5/13のドキュメントを削除した後、最後MacBookAir側にReplicateしてもMacBookAir側の5/13のドキュメントはすぐには削除されません。単に複写をしているだけではないようです。(しばらく時間がたってからReplicateすると相手のDBにも最新の状況が反映されます)

このあたりの動きとして関連して、先のReplicateするときボタンの横にCotinuousトグルがあるので、それをOnにしてReplicateすると連続して同期してくれます。Statusで動作していることが確認できます。

(ただこれは、local→MacBookAirの方向のReplicateなので、localの変更は自動的にMacBookAir側に反映されますが、MacBookAirの変更はlocalに自動的には反映してくれません。)
なお、この設定はマシンを再起動すると消えてしまいます。恒常的にReplicateさせるには別途設定が必要なようです。(当たり前か)






2015年1月2日金曜日

CouchDBのMapReduceをアプリ(python)で動かす

前回、Futon上でMapReduceを定義、動かしてみました。サンプル的にDBの集計をしたい場合はそれでいいんですが、やはりアプリ(python)からMapReduce処理を指示し、集計処理をしてみたいです。

とりあえず前回、日付毎の集計をするMapReduceを”eachdate”に作成しましたので、これをpythonからCallしてみます。(Keyを指定せずに、全部を対象にしています)

  1. # coding: utf-8
  2. import couchdb
  3.  
  4. server = couchdb.Server('http://localhost:5984')
  5. db = server['kakeibo']
  6.  
  7. result = db.view("application/eachdate")
  8.  
  9. for r in result:
  10. print r

ところがこれを実行すると以下の結果になってしまいました。

&lt Row key="None," value="2850">

なんかkeys(日付)毎に集計されるのを期待したのですが、keys無視で集計されています。色々、調べまくってみると、どうもデフォルトではreduce関数の入力はMapの結果だけではなく、Reduceの結果も再度受け入れてしまうようです。(そのため、全部集計してしまう:注)これを期待どおり、keys毎に集計したい場合は以下のOptionを入れるそうです。

  1. # coding: utf-8
  2. import couchdb
  3.  
  4. server = couchdb.Server('http://localhost:5984')
  5. db = server['kakeibo']
  6.  
  7. result = db.view("application/eachdate", group=True)
  8.  
  9. for r in result:
  10. print r

こうすると、期待どおり日付毎の集計をしてくれます。

&lt Row key=u'5/10', value=1300>
&lt Row key=u'5/11', value=650>
&lt Row key=u'5/12', value=900>


(ちなみに、ここにはReduceを行わないoptionとして'reduce=False'というのもあります)

注:Reduce関数の出力が再度Reduce関数の入力に入ってしまう、というのは説明が足りてませんでしたね。(元が英語だったので、意図を解釈しきれてなかった)これは、Mapの結果が大きいと、Map関数が入力データをsplitして複数のMap結果が生成されるそうです。(具体的にどのくらい大きいとsplitされるのかはまだ調べてません。)そうすると、各Map結果に対してReduce関数が動き、最後にそれらのReduce関数の結果をまとめるため、もう一度Reduce関数がCallされるんだそうです。同じReduce関数がCallされるので、そのままだと再集計をしてしまう、ということが起きるようです。それを避けるため、rereduceというflagがあり、Reduce関数内ではそのflagでsplitされたMap関数を処理するReduce関数なのか、最後に集計するReduce関数なのかをif文で識別して動作する、という方法を使うそうです。ここで述べたgroupとsplit、rereduceの関係はまた調べて整理しないとダメですね。

さて、MapReduceのコードをFutonで入力するのもいいですが、python上でコーディングしてしまう方法もあります。

  1. # coding: utf-8
  2. import couchdb
  3.  
  4. server = couchdb.Server('http://localhost:5984')
  5. db = server['kakeibo']
  6.  
  7. map_fun = '''function(doc) {
  8. emit(doc.date, doc.price);
  9. }'''
  10.  
  11. reduce_fun = '''function(keys, values) {
  12. return sum(values);
  13. }'''
  14.  
  15. result = db.query(map_fun, reduce_fun, group=True)
  16.  
  17. for r in result:
  18. print r

何のことはありません、関数定義を文字列として渡しているだけですね。
後、これらの結果を見るとreduce定義の引数のところで(keys, values)としましたが、特に(key, value)でもいいようです。まあわかれば当たり前の話ですが、関数定義の時の引数名なんで、順番が大事なだけですね。


PS:
最後に、ログのとり方についてだけ追記しておきます。特に今回悩んだような症状のとき、Reduce関数には何が引数に入っているか知りたくなります。そんな時、ログ関数があります。

  1. reduce_fun = '''function(keys, values, rereduce) {
  2. log(rereduce);
  3. log(keys);
  4. log(values);
  5. if (rereduce)
  6. return sum(values);
  7. else
  8. return values;
  9. }''

こんなふうにlog()を入れるだけで、/var/log/couchdb/couch.logに記録されます。これを見ると、group=Trueがある時とない時の違いがひと目でわかります。
groupの指定をしていないと以下のログが記録されます。

  1. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
  2. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/10","102d4440a25efcd79349859b67000302"],["5/10","102d4440a25efcd79349859b67000a91"],["5/11","102d4440a25efcd79349859b67000eee"],["5/11","102d4440a25efcd79349859b67001c9b"],["5/12","102d4440a25efcd79349859b67002b7f"]]
  3. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [300,1000,400,250,900]


id情報まで表示されるので見難いのですが、Map関数を通しても、すべての日付のリストがkeysに入っており、対応するpriceもvaluesにリストで5個集められてしまっていることがわかります。(これでは、日付毎の集計なんてされないはずだ)
一方、group=Trueの指定をすると以下のログになりました。

  1. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
  2. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/10","102d4440a25efcd79349859b67000a91"],["5/10","102d4440a25efcd79349859b67000302"]]
  3. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [1000,300]
  4. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.109.0="">] 127.0.0.1 - - POST /kakeibo/_temp_view?group=true 200
  5. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
  6. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/11","102d4440a25efcd79349859b67001c9b"],["5/11","102d4440a25efcd79349859b67000eee"]]
  7. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [250,400]
  8. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
  9. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/12","102d4440a25efcd79349859b67002b7f"]]
  10. [Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [900]


今度は、一回のreduce関数がcallされる時にはkeysには日付ごとのリストが集められていることがわかります。(つまり3回reduce関数はcallされています)これではっきりと、想定した動作をなぜしなかったのかがわかります。