0

jsondiffpatchでJSONの差分を取る

2つのJavaScriptのobjectの差分が取れる。JSONの差分が取れるCLIツールもついている。
https://www.npmjs.com/package/jsondiffpatch


元データ


wikiのページのようなデータを2つ用意する。増えた行、減った行、少し編集された行がある。
let page = {
title: "ruby",
lines: [
{text: "プログラム言語", user: "shokai"},
{text: "http://ruby-lang.org", user: "shokai"},
{text: "111", user: "shokai"},
{text: "2222", user: "shokai"},
{text: "33333", user: "shokai"},
{text: "444444", user: "shokai"}
],
updatedAt: Date.now()
};

let page2 = {
title: "ruby",
lines: [
{text: "プログラミング言語", user: "shokai"},
{text: "テキスト処理に便利", user: "kazusuke"},
{text: "http://ruby-lang.org", user: "shokai"},
{text: "111", user: "shokai"},
{text: "2222", user: "shokai"},
{text: "33333", user: "shokai"}
],
updatedAt: Date.now() + 300000
};


差分を取る、差分をパッチとして使う


jsondiffpatch.diff(left, right) で差分が取れる。patch、unpatchでちゃんと変更を適用できるか確かめる。
配列内のオブジェクトの比較のためにobjectHashという関数が必要。これはオブジェクトに持たせるのではなく、jsondiffpatch.createに引数として渡す。配列の比較をしない場合は必要ない。
https://github.com/benjamine/jsondiffpatch/blob/master/docs/arrays.md
また、patchとunpatch関数はパッチを当てた元データを破壊的に処理してしまうので、cloneしてからpatch/unpatchするようにした。

import {assert} from "chai";
import * as JSONDiffPatch from "jsondiffpatch";
import md5 from "md5"

const jsondiffpatch = JSONDiffPatch.create({
// https://github.com/benjamine/jsondiffpatch/blob/master/docs/arrays.md
objectHash: (obj, index) => {
return md5(obj.text + obj.user);
}
});

// patch/unpatchが元データを破壊的に処理する為必要
function clone(obj){
return JSON.parse(JSON.stringify(obj));
}

let diff = jsondiffpatch.diff(page, page2);

console.log(JSON.stringify(diff, null, 2));

// patchあててもちゃんと同一になる
assert.deepEqual(jsondiffpatch.patch(clone(page), diff), page2);

// patchあててpatch戻す
assert.deepEqual(jsondiffpatch.unpatch(clone(page2), diff), page);

assert.deepEqual(
jsondiffpatch.unpatch(
jsondiffpatch.patch(clone(page), diff)
, diff)
, page);


diffはdeltaというフォーマットになっている。差分なので元データにある1111や2222などの行は含まれない。
{
"lines": {
"0": [
{
"text": "プログラミング言語",
"user": "shokai"
}
],
"1": [
{
"text": "テキスト処理に便利",
"user": "kazusuke"
}
],
"_t": "a",
"_0": [
{
"text": "プログラム言語",
"user": "shokai"
},
0,
0
],
"_5": [
{
"text": "444444",
"user": "shokai"
},
0,
0
]
},
"updatedAt": [
1457784139836,
1457784439836
]
}


MongoDBで使う

まだ試してないけどmongooseのプラグインで、pre-saveで保存前後の差分を取得できるやつがある。
https://www.npmjs.com/package/mongoose-diff

これを使うと最新データのみ保存して、古いデータは変更履歴のみ保存するといったことが楽に実装できそう。

0

JSONシリアライズ方法を設定する

こういうjsonへのシリアライズ方法が書かれてないクラスを使って、to_jsonすると

#!/usr/bin/env ruby
require 'rubygems'
require 'json'

class User
attr_reader :id, :name
def initialize(id, name)
@id = id
@name = name
end
end

users = Array.new
['user1', 'user2', 'shokai'].each_with_index{|name,id|
users.push User.new(id, name)
}

puts users.to_json


クラス名が出てきてしまう
["#<User:0x007fc17b8bda50>","#<User:0x007fc17b8bda28>","#<User:0x007fc17b8bda00>"]


JSON implementation for Rubyに、to_json(*a)を定義しろって書いてあった
#!/usr/bin/env ruby
require 'rubygems'
require 'json'

class User
attr_reader :id, :name
def initialize(id, name)
@id = id
@name = name
end

## JSON作る
def to_json(*a)
{
:id => @id,
:name => @name
}.to_json(*a)
end
end

users = Array.new
['user1', 'user2', 'shokai'].each_with_index{|name,id|
users.push User.new(id, name)
}

puts users.to_json


ちゃんと出てきた
[{"id":0,"name":"user1"},{"id":1,"name":"user2"},{"id":2,"name":"shokai"}]

0

OpenCVで画像サイズを求めるgearman workerをdaemontoolsで管理する

OpenCVで画像のサイズを求めるgearman workerを作って、Rubyから呼ぶで作ったworkerをpreforkさせて、そいつらをdaemontoolsで管理できるようにした。あらかじめCPU個数+いくつかforkしておくと、CPUが複数あるマシンを生かせるし、解析前にlibcurlで画像を取得している時のI/O待ちが少なくなって良い。(この記事のworkerはlibcurl使ってない版だけど)
あと、返り値は自分で作ったjson_builder.hを使って返すようにした。

なにげに大量の画像の中からダウンロード失敗した破損画像を見つけるのに重宝している。

まずdaemontoolsをインストールしておく

gearmandもdaemontoolsで自動起動するようにしておく。


daemontoolsで管理できるようにする。
普通にforkしただけだと、daemontoolsでsvc -dしてプロセスを止めようとしてもforkした子プロセスの方が止まらない。

Perlの場合の良い例があった。
How to manage Gearman worker processes. – TokuLog 改メ tokuhirom’s blog
Parallel::Preforkを使っている。Parallel::Preforkのソースを読んでみたら、trap_signalsオプションで親プロセスがSIGTERMとSIGHUPをフックして、子プロセスにkillを送っていた。
よく考えたら普通のforkで親が子を殺すというやつだった。


Parallel::Preforkと同じ様にやる。
forkした後親が子のpidのリストを持っておいて、SIGTERM/SIGHUPをフックして、子を全部killする処理を追加した。

daemontoolsのrunスクリプトはこれ
#!/bin/sh
exec 2>&1
exec setuidgid sho /Users/sho/src/gearmand-study/imgsize/imgsizeWorker -s localhost -p 7003 --fork 5
起動すると5個にプロセスが増える。親はdaemontoolsのsuperviseが管理してくれる。
これでsvc -dとか-uとかすればまとめて起動終了するようになった。

imgsizeWorker.cpp
// 画像サイズを返すgearman worker
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <iostream>
#include <cv.h>
#include <highgui.h>
#include <boost/program_options.hpp>
#include <boost/regex.hpp>
#include <boost/format.hpp>
#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_io.hpp>
#include <boost/any.hpp>
#include <libgearman/gearman.h>
#include "json_builder.h"

using namespace boost;
using namespace std;

tuple<int, int> get_size(const string& fileName); // 画像のwidth,heightを返す
map<string,any> imgsize(const string& fileName); // gearman workerとしてclientに返すためのJSON Objectを作る
void *job_imgsize(gearman_job_st *job, void *cb_arg, size_t *result_size, gearman_return_t *ret_ptr);
void on_exit_signal(int sig);
vector<int> pids;

int main(int argc, char* argv[]) {
  program_options::options_description opts("options");
  opts.add_options()
    ("help,h", "helpを表示")
    ("server,s", program_options::value<string>(), "gearmanサーバーのアドレス")
    ("port,p", program_options::value<int>(), "gearmanサーバーのport番号")
    ("fork", program_options::value<int>(), "preforkする数")
    ("test,t", program_options::value<string>(), "gearman worker単体テスト用query");
  program_options::variables_map argmap;
  program_options::store(parse_command_line(argc, argv, opts), argmap);
  program_options::notify(argmap);

  if(!argmap.count("help")){
    if(argmap.count("test")){
      cout << "---test---" << endl;
      string gearman_param = argmap["test"].as<string>();
      cout << json_builder::toJson(imgsize(gearman_param)) << endl; // 単体でworkerとしてのテスト
      return 0;
    }else if(argmap.count("server") && argmap.count("port")){
      if(argmap.count("fork")){
int i, pid;
for(i = 1; i < argmap["fork"].as<int>(); i++){
  pid = fork();
  if(pid == 0){ // 子プロセス
    pids.clear();
    break;
  }
  else{ // 親プロセス
    pids.push_back(pid);
    cout << str(format("fork:%d - parent:%d child:%d") % 
i %
getpid() %
pid) << endl;
  }
}
      }
      if(pids.size() > 0){ // 親プロセスの終了シグナルをフックする
signal(SIGTERM, on_exit_signal);
signal(SIGHUP, on_exit_signal);
      }
      gearman_worker_st worker;
      gearman_worker_create(&worker);
      string g_server = argmap["server"].as<string>();
      int g_port = argmap["port"].as<int>();

      struct hostent *g_host = gethostbyname((char*)g_server.c_str());
      string g_server_addr = str(format("%d.%d.%d.%d") %
 (uint)(uchar)g_host->h_addr[0] %
 (uint)(uchar)g_host->h_addr[1] %
 (uint)(uchar)g_host->h_addr[2] %
 (uint)(uchar)g_host->h_addr[3]);

      gearman_worker_add_server(&worker, g_server_addr.c_str(), g_port);
      gearman_worker_add_function(&worker, "img_size", 0, job_imgsize, NULL);
      cout << str(format("---start worker (%s:%d)---") %
  g_server_addr % g_port) << endl;
      while(true) gearman_worker_work(&worker); // workerとして待機
      return 0;
    }
  }
  cerr << "server,portが必要です" << endl;
  cerr << opts << endl;
  return 1;
  
}

// opencvで画像サイズを取得
tuple<int, int> get_size(const string& fileName){
  IplImage *img = cvLoadImage(fileName.c_str());
  if(!img){
    return make_tuple(-1, -1);
  }
  else{
    int width = img->width;
    int height = img->height;
    cvReleaseImage(&img);
    return make_tuple(width, height);
  }
}

// 画像サイズを取得してgearman serverに返すJSON Objectを作る
map<string,any> imgsize(const string& fileName){
  map<string,any> result_m;
  int width, height;
  tie(width, height) = get_size(fileName);
  if(width > 0 && height > 0){
    result_m["width"] = width;
    result_m["height"] = height;
  }
  else{
    result_m["error"] = string("image load error");
  }
  return result_m;
}

// gearman worker job
void *job_imgsize(gearman_job_st *job, void *cb_arg, size_t *result_size, gearman_return_t *ret_ptr){
  string fileName = (char*)gearman_job_workload(job);
  cout << fileName << endl;
  string result_str = json_builder::toJson(imgsize(fileName));
  cout << " => " << result_str << endl;
  char *result = (char*)strdup(result_str.c_str());
  *result_size = result_str.size();
  *ret_ptr = GEARMAN_SUCCESS;
  return result;
}

void on_exit_signal(int sig){
  for(int i = 0; i < pids.size(); i++){
    cout << str(format("kill (pid:%d)") % pids[i]) << endl;
    if(kill(pids[i], SIGKILL) < 0){
      cerr << str(format("kill failed (pid:%d)") % pids[i]) << endl;
    }
  }
  exit(0);
}

0

json_buiilderをgithubに移動させた

bitbucketからhttp://github.com/shokai/json_builder-cppに移動した。

ちょっとまじめにREADMEを書いたりした。
githubのreadmeにはmarkdown形式を使った。markdownをインストールすればローカルでもマークアップがプレビューできて良い

json_builder.hはヘッダファイル単体でstd::mapやvectorをjsonにシリアライズするC++用のライブラリ。parseはできない。

0

json_builderを特殊文字のエスケープ、true、false、nullに対応させた

前:橋本商会 C++でmapやvectorをJSON出力するjson_builder.hを作った


ダブルクオートなどを含む文字列を値に保持するためのエスケープ処理にboost::regexを使ったので、libboost_regex-mt.aをコンパイル時に読み込まないとならなくなった → Makefileの例
まさかboost::regex_replaceで頭にバックスラッシュをつけるのに、バックスラッシュ4つで置換するとは思わなかった


こんな風に使う。true, false, nullを入れられるようになった
test.cpp

#include <iostream>
#include <string>
#include <map>
#include <boost/any.hpp>
#include "../json_builder.h"

int main(int argc, char* argv[]){
  std::map<string,boost::any> user;
  user["name"] = std::string("shokai");
  user["fullname"] = std::string("sho hashimoto");
  user["age"] = 25;
  user["test"] = 1.23;
  user["null"] = json_builder::null;
  user["true"] = true;
  user["false"] = false;

  string json = json_builder::toJson(user);
  cout << json << endl;
  return 0;
}


実行結果
{"age":"25","false":false,"fullname":"sho hashimoto","name":"shokai","null":null,"test":"1.23","true":true}



C++でnullを表現するために適当な構造体を定義してしまったけど、こういうので良いんだろうか?
json_builder.h
#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <boost/any.hpp>
#include <boost/tuple/tuple.hpp>
#include <boost/format.hpp>
#include <boost/foreach.hpp>
#include <boost/regex.hpp>

using namespace std;
using namespace boost;

#define null json_null()

namespace json_builder{
  struct json_null{};

  string toJson(any value){
    if(value.type() == typeid(vector<any>)){
      string result_str;
      vector<any> vec = any_cast<vector<any> >(value);
      for(int i = 0; i < vec.size(); i++){
result_str += toJson(vec[i]);
if(i < vec.size()-1) result_str += ",";
      }
      result_str = str(format("[%s]") % result_str);
      return result_str;
    }
    else if(value.type() == typeid(map<string,any>)){
      string result_str;
      map<string,any> m = any_cast<map<string,any> >(value);
      string key;
      any value;
      int i = 0;
      BOOST_FOREACH(tie(key,value), m){
result_str += str(format("\"%s\":%s") % key % toJson(value));
if(++i < m.size()) result_str += ",";
      }
      result_str = str(format("{%s}") % result_str);
      return result_str;
    }
    else if(value.type() == typeid(json_null)){
      return string("null");
    }
    else if(value.type() == typeid(string)){
      return str(format("\"%s\"") % 
   regex_replace(any_cast<string>(value), regex("[\"\'\\\\/]"), "\\\\$0"));
    }
    else if(value.type() == typeid(bool)){
      if(any_cast<bool>(value)) return string("true");
      return string("false");
    }
    else if(value.type() == typeid(int)){
      return str(format("\"%d\"") % any_cast<int>(value));
    }
    else if(value.type() == typeid(double)){
      return str(format("\"%d\"") % any_cast<double>(value));
    }
  }

}