2014年12月29日 星期一

MongoDB 開發筆記 - 使用 MapReduce 進行 Matrix Multiplication (矩陣乘法) 運算

最近下了班有空時都會繼續專研 MongoDB 分析資料的部分,一開始完全都用 Aggregation ,實在是有一些實測的效能比 MapReduce 好很多,但對於一些較複雜的計算方式,一直用 Aggregation 反而會卡住或是增加資料結構的複雜度 Orz 總之,開始正式去寫一些簡單的 MongoDB MapReduce 程式。

把玩的過程中,發現先前設計的 Sparse Matrix 結構,在計算矩陣相乘時,花了不少步驟並且效果變成永遠只能有一隻 Mapper 在進行 Orz 完全不能平行處理,因此開始研究一下如何用 MapReduce 進行矩陣運算。

網路上資源果然豐富,找到一篇:Programming Puzzle: Multiply Matrixes with MongoDB,概念解釋得很清楚。簡言之,當矩陣相乘時,以 A x B = C 為例,在組成 C 矩陣內的元素過程中,會採用 A 跟 B 矩陣內的部分元素相乘,那麼試著找個關係式子,讓這些元素最後在 Reducer 裡撞在一起,撞在一起時再相乘即可。

原來如此簡單的想法就可以讓 MapReduce 架構處理矩陣相乘了!有興趣可以再參考一下:changyy/node-mongodb-matrix-multiplication

2014年12月28日 星期日

MongoDB 開發筆記 - 在 MapReduce 進行 debug 的一些心得

有點久沒寫 blog 了!最近有空時就是把玩 MongoDB 或是嘗試送一些 patch ,所以反而就少寫一些筆記,因為都改成用 Code 啦 :P

最近玩 Mongodb MapReduce 時,碰到幾個問題跟需求:
  • 碰到 mongod Out of memory
  • 想要在 Mapper 跟 Reducer 輸出一些 console 訊息
其中 out of memory 的部分,經過 trace 後發現是內部 Mapper 實作的架構採用 C++ Map Structure 紀錄結果,累計處理完一百筆資料會輸出,但如果單一個筆 input 會產生極大數量的結果時,就會出現 out of memory,已經送了個鳥鳥的 patch 出去,就看 MongoDB Reviewer 決定如何 :P 畢竟這種現象也可以歸屬在使用者資料設計問題,為何讓一筆 input 產生巨量資料 XD

至於現況怎樣解套?若碰到 mongod crash 問題,先試試把 jsMode 打開吧!因為 jsMode = true 時,將採用 v8 engine 處理 mapper function,並且資料是直接輸出到 temp collection 的,可避開 out of memory 問題。

對於前期開發時,想在 Mapper 跟 Reducer 印出一些訊息時,此時不能直接用 console.log(obj),要改用 print(JSON.stringify(obj)) ,並且去翻一下 

/var/log/mongodb/mongod.log 吧!

2014年11月25日 星期二

iOS 開發筆記 - UIWebview loadData 以及 local resource 配置

很久以前把玩過 PhoneGap 後,就很久沒使用 UIWebview 來當作主要 UI。這次嘗試使用 Web UI 當作操作介面,於是乎就碰到使用 jQuery 的問題 XD 偷懶的解法就是用 remote resource,更佳的解法是用 local file。

使用方式:

// 記得要 Added folders 要採用 Create folder references
// web-resource/index.html
// web-resource/jquery.min.js
//
NSString *localWebPagePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"web-resource"];

[self.webview loadData:[NSData dataWithContentsOfFile:localWebPagePath] MIMEType:@"text/html" textEncodingName:@"UTF-8" baseURL:[NSURL URLWithString:localWebPagePath]];


其中的 baseURL 只要填寫正確,在 index.html 裡頭就可以用相對路徑存取 local file 了。

例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="jquery.min.js"></script>
</head>
<body>
...
</body>
</html>

[PHP] CodeIgniter 與 Facebook PHP SDK v4 整合筆記 @ Ubuntu 14.04

整個流程並主要是把 Facebook PHP sdk v4 source code 下載到 ci_proj/application/libraries 中,接著撰寫一隻 ci library: facebook.php 來橋接一下,就差不多可以收工了。在此謹驗證收到的 facebook token 是否有效,其他部分並不實作:

$ cd /path/ci_proj/application/libraries
$ git clone https://github.com/facebook/facebook-php-sdk-v4.git
$ ln -s facebook-php-sdk-v4 facebook


建立 Facebook 相關 config:

$ vim /path/ci_proj/application/config/facebook.php
<?php defined('BASEPATH') OR exit('No direct script access allowed');
$config['facebook']['api_id'] = 'FACEBOOK APP ID';
$config['facebook']['app_secret'] = 'FACEBOOK APP SECRET KEY';


編輯橋接的 library 用法:

$ vim /path/ci_proj/application/libraries/facebook.php

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
require_once APPPATH . 'libraries/facebook/autoload.php';

use Facebook\FacebookRedirectLoginHelper;
use Facebook\FacebookSession;
use Facebook\FacebookRequest;
use Facebook\GraphUser;
use Facebook\FacebookRequestException;

class Facebook {
var $ci;
var $helper;
var $session;
var $permissions;

public function __construct() {
$this->ci =& get_instance();
$this->ci->load->config('facebook');
$this->permissions = $this->ci->config->item('permissions', 'facebook');
FacebookSession::setDefaultApplication( $this->ci->config->item('api_id', 'facebook'), $this->ci->config->item('app_secret', 'facebook') );
        }
public function token_validation($token) {
try {
$this->session = new FacebookSession( $token );
if ( $this->session->validate() )
return true;
} catch ( Exception $e ) {
}
$this->session = null;
return false;
}
public function get_user($token = NULL) {
if ( $this->session || (!empty($token) && $this->token_validation($token)) ) {
$request = ( new FacebookRequest( $this->session, 'GET', '/me' ) )->execute();
$user = $request->getGraphObject()->asArray();
return $user;
}
return false;
}
}


使用方式:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Test extends CI_Controller {
public function index()
{
$this->load->config('facebook');
$this->load->library('facebook');
$output = array(
'reqeusts' => $_REQUEST,
'app_id' => $this->config->item('api_id', 'facebook'),
'user_id' => $this->facebook->get_user($_REQUEST['token'])
);
$this->output->set_content_type('application/json')->set_output(json_encode($output));
        }
}

[PHP] 使用 CodeIgniter 和 codeigniter-restserver 建立 RESTful API 以及測試方式 @ Ubuntu 14.04

環境資訊:

  • Ubuntu 14.04 64 Bit (lsb_release -a)
  • PHP 5.5.9 (php -v)
  • CodeIgniter 2.2.0 (echo CI_VERSION)

接著,透過 github.com/chriskacerguis/codeigniter-restserver 擴充 CodeIgniter:

$ wget https://raw.githubusercontent.com/chriskacerguis/codeigniter-restserver/master/application/config/rest.php -O /path/ci_proj/application/config/rest.php

$ wget https://raw.githubusercontent.com/chriskacerguis/codeigniter-restserver/master/application/libraries/Format.php -O /path/ci_proj/application/libraries/Format.php

$ wget https://raw.githubusercontent.com/chriskacerguis/codeigniter-restserver/master/application/libraries/REST_Controller.php -O /path/ci_proj/application/libraries/REST_Controller.php


接著,請確認 application/config/rest.php 設定:

$config[rest_default_format] = 'json'; // 預設為 xml

之後,就可以撰寫簡單的 RESTful API 啦:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
require(APPPATH.'/libraries/REST_Controller.php');

class Item extends REST_Controller {
        public function index_get() {
                $this->list_get();
        }
        public function index_post() {
                $this->add_post();
        }
        public function index_put() {
                $this->update_put();
        }
        public function index_delete() {
                $this->delete_delte();
        }

        public function list_get() {
                echo "list";
        }
        public function add_post() {
                echo "create";
        }
        public function update_put() {
                echo "put";
        }
        public function remove_delete() {
                echo "remove";
        }
}


測試方式:

取得清單(GET)

$ curl -X GET http://localhost/ci_proj/item/list
$ curl -X GET http://localhost/ci_proj/item/

新增資料(POST)

$ curl -X POST http://localhost/ci_proj/item/add
$ curl -X POST http://localhost/ci_proj/item/


更新資料(PUT)

$ curl -X PUT http://localhost/ci_proj/item/update
$ curl -X PUT http://localhost/ci_proj/item/


刪除資料(DELETE)

$ curl -X DELETE http://localhost/ci_proj/item/delete
$ curl -X DELETE http://localhost/ci_proj/item/


也可以用 wget --method GET/POST/PUT/DELETE 等進行測試。

至於 RESTful 常見的 HTTP STATUS CODE,可以參考 Using HTTP Methods for RESTful Services

例如:

取得清單成功回應 200 OK,失敗則為 404 NOT FOUND;新增資料成功 200 OK 或是 201 LOCATION 到新增項目的 URI,失敗為 404 NOT FOUND;更新資料成功為 200 OK,失敗為 404 NOT FOUND;刪除資料成功為 200 OK,失敗為 404 NOT FOUND。

接著,是設計每個 GET, POST, PUT, DELETE API 細節,可以分別使用 $this->get('param')、$this->post('param')、$this->put('param') 和 $this->delete('param') 取得對應資料,而回應時,需要的 HTTP Status Code 可以使用:

$this->response( array('status' => true), 200);
$this->response( array('status' => false, 'error' => 'error message'), 400);
$this->response( array('status' => false, 'error' => 'error message'), 404);


若要 debug 的話,大概可以用:

$this->response( array('status' => true, 'get' => $this->get(), 'post' => $this->post(), 'put' => $this->put(), 'delete' => $this->delete() ), 200);

搭配 curl -i -X GET|POST|PUT|DELETE -d 'key=value' 方式進行,除了可以看到 HTTP Response code 外,也能看到 get, post, put, delete 各種參數資訊:

$ curl -i -X DELETE -d "haha=hehe" CGI
HTTP/1.1 200 OK
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.5
Content-Length: 68
Content-Type: application/json; charset=utf-8

{"status":true,"get":[],"post":[],"put":[],"delete":{"haha":"hehe"}}

2014年11月20日 星期四

[Linux] Docker 使用筆記 - 透過 Supervisor 開機執行多項服務(SSH, Apache Web Server, MySQL) @ Ubuntu 14.04

前陣子把玩 Docker 時,粗略知道運作是著重落在一隻 process 上,在 Dockerfile 中雖然可以執行多個 CMD,但擺上多隻會共同運行指令仍是無效的。解法的原理就是執行一隻程式,由那支程式管理其他程式。在 Docker 官方文件上就是使用 Supervisor 啦:Using Supervisor with Docker

Dockefile:

FROM ubuntu:14.04
RUN apt-get update
RUN sudo -E "DEBIAN_FRONTEND=noninteractive" apt-get install -y telnet curl mysql-server-5.6 apache2 php5 php5-mysql openssh-server supervisor
RUN mkdir -p /var/lock/apache2 /var/run/apache2 /var/run/mysqld /var/run/sshd /var/log/supervisor

COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

RUN mkdir -p /root/.ssh/ && chmod 700 /root/.ssh/
COPY id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 600 /root/.ssh/authorized_keys

EXPOSE 22 80 3306
CMD ["/usr/bin/supervisord"]


supervisord.conf:

[supervisord]
nodaemon=true

[program:sshd]
command=/usr/sbin/sshd -D

[program:apache2]
command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND"

[program:mysql]
command=/usr/bin/pidproxy /var/run/mysqld/mysqld.pid /usr/sbin/mysqld
autorestart=true


id_rsa / id_rsa.pub:

$ ssh-keygen -t rsa -f id_rsa -P''

上述檔案都在同一層目錄中,分別有 Dockefile, supervisord.conf, id_rsa 和 id_rsa.pub ,共四個檔案。

接著建立 Image:

$ cd /path/dir
$ sudo docker build -t dev .


運行,將本機端的 10022, 10080, 13306 分別對應到 Docker Container 的 22, 80, 3306:

$ sudo docker run -t -p 10022:22 -p 10080:80 -p 13306:3306 dev
/usr/lib/python2.7/dist-packages/supervisor/options.py:295: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security.
  'Supervisord is running as root and it is searching '
CRIT Supervisor running as root (no user in config file)
WARN Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing
INFO RPC interface 'supervisor' initialized
CRIT Server 'unix_http_server' running without any HTTP authentication checking
INFO supervisord started with pid 1
INFO spawned: 'sshd' with pid 9
INFO spawned: 'mysql' with pid 10
INFO spawned: 'apache2' with pid 11
INFO success: sshd entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
INFO success: mysql entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
INFO success: apache2 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
...


也可以按 ctrl+c 離開,但 VM 還會繼續跑

遠端登入:

$ ssh -i /path/dir/id_rsa root@localhost:10022

若有需求想要 console 端登入:

$ sudo docker ps -a
$ sudo docker inspect --format '{{.State.Pid}}' CONTAINER_ID
$ sudo nsenter --target PID_NUMBER --mount --uts --ipc --net --pid


收工!

2014年11月13日 星期四

[SQL] 從 Table 中取出 Column 1 取資料並塞進 Column 2 方法 @ MySQL 5.6

有一張 table 儲存 field1, field2 為原始資料,由於資料尚未驗證,想要透過 REGEXP 驗證資料結果,這時就想到每次找都要驗證一次有點煩,所以就乾脆建立 field3,將驗證過的資料儲存起來。

至於解法方面,就單純把撈出來的資料再 INSERT 回去 :P 假設有一張 Table 名為 mTable,其中 key 為 field1, field2,而 field3 預設可以為空,目標則是將 field1 驗證過的資料儲存在 field3,未來使用時就可以透過 field3 來當作資料驗證。

新增資料用法:

INSERT INTO (field1, field2) VALUES ... ;

驗證 field1 資料,把正確格式擺進 field3:

INSERT mTable (field1, field2, field3)

SELECT field1, field2, field1 AS field3 FROM mTable WHERE field3 IS NULL AND field1 REGEXP '^[0-9A-Za-z]+$'

ON DUPLICATE KEY UPDATE

field3 = VALUES(field3)


如此一來,就完成驗證 :P

接著應該也可以嘗試 MySQL trigger 用法?不過,驗證資料這件事應該不要叫 DB 做才對 XD 只是...環境所限啊。

2014年11月12日 星期三

[Linux] 使用 awk, sort, uniq, grep, dig 批次驗證 email address @ Ubuntu 14.04

工作時中,偶爾會對 UNIX 系統感動! 雖然有時很簡單的任務會龜毛到硬要連續指令來處理,明明寫一小段 code 就好了啊 Orz

取處已知清單中的 email domain:

$ mysql -e "select email from db.user;" > email.log;
$ sort email.log | uniq | awk -F'@' '{print $2}' | sort | uniq | grep "." > email.domain.log


最後的 grep "." 則是要避開 hostname 不合法的項目(如 localhost 等)。

驗證 mx:

$ dig -t mx test.com +short | wc -l
$ test `dig -t mx test.com +short | wc -l` -ne 0 && echo "ok"


批次驗證:

$ awk '{ system("test `dig -t mx "$1" +short | wc -l` -ne 0 && echo "$1)}' email.domain.log > email.validated.log

2014年11月11日 星期二

[Linux] c++: internal compiler error: Killed (program cc1plus) @ Ubuntu 14.04

最近 編譯 C++ 的 source code 時,開始會蹦出奇怪的狀態:

c++: internal compiler error: Killed (program cc1plus)
Please submit a full bug report,
with preprocessed source if appropriate.

See <file:///usr/share/doc/gcc-4.8/README.Bugs> for instructions.


一開始碰過一兩次後,重開機真的又沒事,但些會開始又常碰到,接著猜想是不是 memory 不夠了,除了用 free -m 觀察外,也把一些 daemon 關掉,真的猜中了 :P

大概是因為我租的機器只有 512MB RAM 啦 Orz

2014年11月8日 星期六

[Linux] 在 UTF8 Terminal 環境下,找尋 Big5 檔案內的關鍵字 @ Ubuntu 14.04

最近想要研究一些老檔案,都是 Big5 編碼,接著就想著到底該怎樣叫 grep 幫你查資料,因為 pattern 要用 Big5 編碼,但 Terminal 是 UTF-8 環境。

我想到的是先把 pattern 轉成 big5 後,透過 xxd 轉乘 hex code,然後一樣將 input 透過 xxd 輸出後用 grep 去找,結果堪用,但不是很方便。

問一下高手,高手獻技:

$ grep -r `echo -ne "大學" | piconv -f utf8 -t big5` target_files

還差一點,因為 Terminal 環境編碼關係,解法:

$ LC_ALL=C grep -r `echo -ne "大學" | piconv -f utf8 -t big5` target_files

此時 Terminal 輸出會亂掉,所以再把 output 從 Big5 轉成 UTF-8:

$ LC_ALL=C grep -r `echo -ne "大學" | piconv -f utf8 -t big5` target_files | piconv -f big5 -t utf8

若想要有 grep highlight ,那就再叫 grep 做一次吧 XD

$ LC_ALL=C grep -r `echo -ne "大學" | piconv -f utf8 -t big5` target_files | piconv -f big5 -t utf8 | grep "大學"

收工!

2014年11月5日 星期三

[Linux] MySQL Server 5.6 更換 DB Location @ Ubuntu 14.04

在 AWS EC2 叫機器出來時,大多都會用 EBS 來擴充空間,對於 mysql server 預設裝在 /var/lib/mysql 時,就容易碰到空間不足的時候。解法:手動更換 mysql db location 吧 :P

順便把 EBS 處理過程也都列一列,假設已經新增並掛載 EBS 空間:

$ sudo lsblk
NAME    MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
...
xvdb    202:16   0     4G  0 disk /mnt
xvdc    202:32   0   500G  0 disk

$ sudo file -s /dev/xvdc
$ sudo mkfs -t ext3 /dev/xvdc
$ sudo mkdir /data
$ sudo mount -t ext3 /dev/xvdc /data
$ sudo chmod 777 /da
$ sudo vim /etc/fstab
/dev/xvdc /data ext3 defaults,nofail 0 2


處理 MySQL Server:

$ sudo apt-get install mysql-server-5.6
$ sudo service mysql stop
$ sudo mv /var/lib/mysql /data/mysql
$ sudo ln -s /data/mysql /var/lib/mysql
$ sudo vim /etc/apparmor.d/usr.sbin.mysqld
  /var/lib/mysql/ r,
  /var/lib/mysql/** rwk,
  /var/log/mysql/ r,
  /var/log/mysql/* rw,

  /data/mysql r,
  /data/mysql** rwk,
  /data/mysql r,
  /data/mysql* rwk,
$ sudo service apparmor restart
$ sudo service mysql start

2014年11月3日 星期一

[Linux] 架設 MongoDB Sharded Cluster 與驗證資料分散性 @ Ubuntu 14.04

今年年初時,把玩過 MongoDB Replica Set ,也試過一些 Map-Reduce 計算等,最近終於又開始抽空實驗了 :P 當時看到不少 Sharded Cluster 搭建在 MongoDB Replica Set 身上,深感 process (server) 的數量不低,就遲遲沒跳下去玩,最近才發現就算只是單一個 MongoDB Server 也可以當作 shared Cluster 一員,就方便啦!

此次 MongoDB Sharded Cluster 架構:
  • 跑兩個 MongoDB Server,分別在 port 10001, 10002
  • 跑一個 MongoDB Config Server - port 20001
  • 跑一個 MongoDB Routing Server - port 30001
以下操作全部都在一台 ubuntu 14.04 server:

$ rm -rf /tmp/mongo && mkdir -p /tmp/mongo/db/1 /tmp/mongo/db/2 /tmp/mongo/db/c /tmp/mongo/log

$ mongod --port 10001 --dbpath /tmp/mongo/db/1 --logpath /tmp/mongo/log/1 &
$ mongod --port 10002 --dbpath /tmp/mongo/db/2 --logpath /tmp/mongo/log/2 &
$ mongod --port 20001 --configsvr --dbpath /tmp/mongo/db/c --logpath /tmp/mongo/log/c &

$ mongos --port 30001 --configdb 127.0.0.1:20001 &


設定 partition 用法:

$ mongo --port 30001
mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "version" : 4,
        "minCompatibleVersion" : 4,
        "currentVersion" : 5,
        "clusterId" : ObjectId("###############")
}
  shards:
  databases:
        {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
mongos> sh.addShard('127.0.0.1:10001')
{ "shardAdded" : "shard0000", "ok" : 1 }
mongos> sh.addShard('127.0.0.1:10002')
{ "shardAdded" : "shard0001", "ok" : 1 }
mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "version" : 4,
        "minCompatibleVersion" : 4,
        "currentVersion" : 5,
        "clusterId" : ObjectId("#####################")
}
  shards:
        {  "_id" : "shard0000",  "host" : "127.0.0.1:10001" }
        {  "_id" : "shard0001",  "host" : "127.0.0.1:10002" }
  databases:
        {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
mongos> sh.enableSharding('ydb')
{ "ok" : 1 }
mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "version" : 4,
        "minCompatibleVersion" : 4,
        "currentVersion" : 5,
        "clusterId" : ObjectId("######################")
}
  shards:
        {  "_id" : "shard0000",  "host" : "127.0.0.1:10001" }
        {  "_id" : "shard0001",  "host" : "127.0.0.1:10002" }
  databases:
        {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
        {  "_id" : "test",  "partitioned" : false,  "primary" : "shard0000" }
        {  "_id" : "ydb",  "partitioned" : true,  "primary" : "shard0000" }
mongos> use ydb
switched to db ydb
mongos> db.ycollection.ensureIndex( { _id : "hashed" } )
{
        "raw" : {
                "127.0.0.1:10001" : {
                        "createdCollectionAutomatically" : true,
                        "numIndexesBefore" : 1,
                        "numIndexesAfter" : 2,
                        "ok" : 1
                }
        },
        "ok" : 1
}
mongos> sh.shardCollection("ydb.ycollection", { "_id": "hashed" } )
{ "collectionsharded" : "ydb.ycollection", "ok" : 1 }
mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "version" : 4,
        "minCompatibleVersion" : 4,
        "currentVersion" : 5,
        "clusterId" : ObjectId("#####################")
}
  shards:
        {  "_id" : "shard0000",  "host" : "127.0.0.1:10001" }
        {  "_id" : "shard0001",  "host" : "127.0.0.1:10002" }
  databases:
        {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
        {  "_id" : "test",  "partitioned" : false,  "primary" : "shard0000" }
        {  "_id" : "ydb",  "partitioned" : true,  "primary" : "shard0000" }
                ydb.ycollection
                        shard key: { "_id" : "hashed" }
                        chunks:
                                shard0000       2
                                shard0001       2
                        { "_id" : { "$minKey" : 1 } } -->> { "_id" : NumberLong("-4611686018427387902") } on : shard0000 Timestamp(2, 2)
                        { "_id" : NumberLong("-4611686018427387902") } -->> { "_id" : NumberLong(0) } on : shard0000 Timestamp(2, 3)
                        { "_id" : NumberLong(0) } -->> { "_id" : NumberLong("4611686018427387902") } on : shard0001 Timestamp(2, 4)
                        { "_id" : NumberLong("4611686018427387902") } -->> { "_id" : { "$maxKey" : 1 } } on : shard0001 Timestamp(2, 5)


簡易驗證資料分散性:

mongos>

db.ycollection.insert({user:"a", item: "1", rate: 0.3})
db.ycollection.insert({user:"a", item: "3", rate: 0.5})
db.ycollection.insert({user:"a", item: "4", rate: 0.9})
db.ycollection.insert({user:"b", item: "2", rate: 0.1})
db.ycollection.insert({user:"b", item: "3", rate: 0.6})
db.ycollection.insert({user:"b", item: "5", rate: 0.2})
db.ycollection.insert({user:"c", item: "3", rate: 0.2})
db.ycollection.insert({user:"c", item: "4", rate: 0.7})

mongos> db.ycollection.find()
{ "_id" : ObjectId("#################"), "user" : "a", "item" : "3", "rate" : 0.5 }
{ "_id" : ObjectId("#################"), "user" : "a", "item" : "1", "rate" : 0.3 }
{ "_id" : ObjectId("#################"), "user" : "a", "item" : "4", "rate" : 0.9 }
{ "_id" : ObjectId("#################"), "user" : "b", "item" : "2", "rate" : 0.1 }
{ "_id" : ObjectId("#################"), "user" : "b", "item" : "5", "rate" : 0.2 }
{ "_id" : ObjectId("#################"), "user" : "b", "item" : "3", "rate" : 0.6 }
{ "_id" : ObjectId("#################"), "user" : "c", "item" : "3", "rate" : 0.2 }
{ "_id" : ObjectId("#################"), "user" : "c", "item" : "4", "rate" : 0.7 }


接著,想要驗證資料的確是儲存在不同台 mongodb server ,作法就是把資料透過 mongodump 出來(bson),再用 bsondump 印出來驗證:

$ cd /tmp
$ mongodump --port 10001 -d ydb -c ycollection
$ bsondump dump/ydb/ycollection.bson
{ "_id" : ObjectId( "#############" ), "user" : "a", "item" : "1", "rate" : 0.3 }
{ "_id" : ObjectId( "#############" ), "user" : "b", "item" : "2", "rate" : 0.1 }
{ "_id" : ObjectId( "#############" ), "user" : "b", "item" : "3", "rate" : 0.6 }
{ "_id" : ObjectId( "#############" ), "user" : "c", "item" : "4", "rate" : 0.7 }
4 objects found

$ mongodump --port 10002 -d ydb -c ycollection
$ bsondump dump/ydb/ycollection.bson
{ "_id" : ObjectId( "#############" ), "user" : "a", "item" : "3", "rate" : 0.5 }
{ "_id" : ObjectId( "#############" ), "user" : "a", "item" : "4", "rate" : 0.9 }
{ "_id" : ObjectId( "#############" ), "user" : "b", "item" : "5", "rate" : 0.2 }
{ "_id" : ObjectId( "#############" ), "user" : "c", "item" : "3", "rate" : 0.2 }
4 objects found


因此可完成驗證,資料的確分在不同區!有興趣還可以用 MongoDB Aggregate (Map-Reduce) 實作共現矩陣(Co-Occurrence Matrix)及簡易的推薦系統 來驗證 partition 演算法的正確性。

2014年10月30日 星期四

[MySQL] Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'

些前在阿里雲、AWS RDS 架設 MySQL Master-Master Replication 出現此問題 Orz 將兩邊設定為各是對方的 Master 如此達到資料跨國同步。然而,卻發現在阿里雲作為 Replication Slave 角色的出現這個問題 Orz 由於在阿里雲的 DB Server 偏向 Cache 角色,確認後資料屬性,決定解決的方案是將阿里雲機器清空再重 AWS RDS 匯入,筆記一下解決步驟:

  1. 兩邊都停下 slave 角色動作
    • ALiYun-DB> STOP SLAVE
    • AWS-RDB> CALL mysql.rds_stop_replication
  2. 從 AWS RDS 匯出資料、並查看 MASTER_LOG_FILE 和 MASTER_LOG_POS 資訊
  3. 匯入阿里雲 DB Server,並設定 Slave 角色資訊,啟動 Slave 工作
    • $ mysql -h AliYunDB < AWSRDBExport.sql
    • ALiYun-DB> CHANGE MASTER TO MASTER_LOG_FILE='aws-rdb-mysql-bin-changelog.#######', MASTER_LOG_POS=#####;
    • ALiYun-DB> START SLAVE
    • ALiYun-DB> SHOW SLAVE STATUS \G
  4. 查看阿里雲 MASTER_LOG_FILE 和 MASTER_LOG_POS,更新 AWS RDS Slave 角色資訊,重新啟動 AWS RDS Slave 工作
    • ALiYun-DB> SHOW MASTER STATUS \G
      • MASTER_LOG_FILE
      • MASTER_LOG_POS
    • AWS-RDB> CALL mysql.rds_reset_external_master;
    • AWS-RDB> CALL mysql.rds_set_external_master (
          'aliyun-db-server-ip',
          3306,
          'repl_account',
          'repl_password',
          'aliyun-db-MASTER_LOG_FILE',
          aliyun-db-MASTER_LOG_POS
      ,   0 )
    • AWS-RDB > CALL mysql.rds_start_replication;
    • AWS-RDS > CALL mysql.rds_set_configuration('binlog retention hours', 24*14);

2014年10月29日 星期三

[Linux] 使用 MongoDB Aggregate (Map-Reduce) 實作共現矩陣(Co-Occurrence Matrix)及簡易的推薦系統 @ Ubuntu 14.04


這陣子查閱了一些推薦系統的作法,發現早在 2012 年網路上就有非常多 Apache Mahout 的使用心得,這一套已經內含許多常見的演算法精華,故許多人都是採用 Apache Mahout 搭建推薦系統的。至於為何還要自己刻一個出來?單純練功啦,在此就練練 Partition、Sort 和 Merge (Map-Reduce) 的分析方式,但採用 MongoDB Aggregate 實作。

此例挑選 user, item 的關聯紀錄,例如 user1 買了 item1, item2, item5,而 user2 買了 item3, item4 等。在推薦系統中,大多就是將 input 前置處理,在挑恰當的演算法加以計算,接著處理 output。在此採用共現矩陣(Co-Occurrence Matrix) 的用法,以 user, item 的關聯紀錄,可以統計出一個 Item-based Co-Occurrence Matrix,簡言之就是各個 item 之間的關係,例如透過 item 1 跟 item 2 一起出現現象,整理成一個計算方式。

假設共有 a, b, c 三個使用者,而有 1, 2, 3, 4, 5,共有五種物品,其中 a, b, c 分別購買部分用品,並對各物品的評價(或價錢等意義)如下:

a, 1, 0.3
a, 3, 0.5
a, 4, 0.9

b, 2, 0.1
b, 3, 0.6
b, 5, 0.2

c, 3, 0.2
c, 4, 0.7


其中, 共現矩陣(Co-Occurrence Matrix) 如下:

\  1, 2, 3, 4, 5
1: 1, 0, 1, 1, 0
2: 0, 1, 1, 0, 1
3: 1, 1, 3, 2, 1
4: 1, 0, 2, 2, 0
5: 0, 1, 1, 0, 1


(1, 1) = 1, 代表 1 僅出現在 1 個使用者的清單
(4, 4) = 2, 代表 4 出現在 2 個使用者的清單
(3, 3) = 3, 代表 3 出現在 3 個使用者的清單
(4, 3) = 2, 代表 4 跟 3 同時出現的次數有兩次

接著,把使用者的購物清單也向量化(或矩陣):

\    a,   b,   c
1: 0.3,   0,   0,
2:   0, 0.1,   0,
3: 0.5, 0.6, 0.2,
4: 0.9,   0, 0.7,
5:   0, 0.2,   0,


當這兩個矩陣相成的結果,即為使用者與物品的關聯成果,且每個物品的分數即可當作使用者感興趣的分數。


以 MongoDB 為例,先將資料一筆筆新增至 db.rec 中:

db.rec.drop()
db.rec.insert({user:"a", item: "1", rate: 0.3})
db.rec.insert({user:"a", item: "3", rate: 0.5})
db.rec.insert({user:"a", item: "4", rate: 0.9})
db.rec.insert({user:"b", item: "2", rate: 0.1})
db.rec.insert({user:"b", item: "3", rate: 0.6})
db.rec.insert({user:"b", item: "5", rate: 0.2})
db.rec.insert({user:"c", item: "3", rate: 0.2})
db.rec.insert({user:"c", item: "4", rate: 0.7})

> db.rec.find()
{ "_id" : ObjectId("5450dedfbdfb5bad287c953c"), "user" : "a", "item" : "1", "rate" : 0.3 }
{ "_id" : ObjectId("5450dedfbdfb5bad287c953d"), "user" : "a", "item" : "3", "rate" : 0.5 }
{ "_id" : ObjectId("5450dedfbdfb5bad287c953e"), "user" : "a", "item" : "4", "rate" : 0.9 }
{ "_id" : ObjectId("5450dedfbdfb5bad287c953f"), "user" : "b", "item" : "2", "rate" : 0.1 }
{ "_id" : ObjectId("5450dedfbdfb5bad287c9540"), "user" : "b", "item" : "3", "rate" : 0.6 }
{ "_id" : ObjectId("5450dedfbdfb5bad287c9541"), "user" : "b", "item" : "5", "rate" : 0.2 }
{ "_id" : ObjectId("5450dedfbdfb5bad287c9542"), "user" : "c", "item" : "3", "rate" : 0.2 }
{ "_id" : ObjectId("5450dee0bdfb5bad287c9543"), "user" : "c", "item" : "4", "rate" : 0.7 }


接著分析 user 跟 item 的個數,分別儲存在 db.user 跟 db.item 中:

> db.item.drop()
true
> db.rec.aggregate([{$group: {_id: "$item"}}, {$sort: {_id: 1}}]).forEach(function(input) {
db.item.insert(input);
})
> db.item.find()
{ "_id" : "1" }
{ "_id" : "2" }
{ "_id" : "3" }
{ "_id" : "4" }
{ "_id" : "5" }

> db.user.drop()
> db.rec.aggregate([{$group: {_id: "$user"}}, {$sort: {_id: 1}}]).forEach(function(input) {
db.user.insert(input);
})
> db.user.find()
{ "_id" : "a" }
{ "_id" : "b" }
{ "_id" : "c" }


建立 Item-based Co-Occurrence Matrix(在此採用sparse matrix):

> db.user_item_list.drop();
> db.rec.aggregate({ $group: {_id: "$user", items: {$push: "$item"} }}).forEach(function(input){
db.user_item_list.insert(input);
})

> db.comatrix_input.drop()
> db.user_item_list.find().forEach(function(input){
for (var i in input.items){
var key = input.items[i]+"-"+input.items[i];
db.comatrix_input.insert( { key: key, value: 1} );
for (var j in input.items){
if (j>i) {
var key = input.items[i]+"-"+input.items[j];
db.comatrix_input.insert( { key: key, value: 1} );
key = input.items[j]+"-"+input.items[i];
db.comatrix_input.insert( { key: key, value: 1} );
}
}
}
});
> db.comatrix.drop()
> db.comatrix_input.aggregate({ $group: {_id: "$key", value: {$sum: "$value"}}}).forEach(function(input) {
db.comatrix.insert(input)
});
> db.comatrix.find()
{ "_id" : "1-3", "value" : 1 }
{ "_id" : "3-5", "value" : 1 }
{ "_id" : "5-2", "value" : 1 }
{ "_id" : "1-4", "value" : 1 }
{ "_id" : "3-1", "value" : 1 }
{ "_id" : "3-2", "value" : 1 }
{ "_id" : "2-5", "value" : 1 }
{ "_id" : "2-3", "value" : 1 }
{ "_id" : "1-1", "value" : 1 }
{ "_id" : "3-3", "value" : 3 }
{ "_id" : "5-3", "value" : 1 }
{ "_id" : "4-1", "value" : 1 }
{ "_id" : "4-3", "value" : 2 }
{ "_id" : "3-4", "value" : 2 }
{ "_id" : "5-5", "value" : 1 }
{ "_id" : "4-4", "value" : 2 }
{ "_id" : "2-2", "value" : 1 }


透過共現矩陣推算 user 對 item 的喜好程度:

> db.user_prefer.drop()
> db.item.find().forEach(function(in1) {
var value = 0;
db.item.find().forEach(function(in2) {
var key = in1._id+"-"+in2._id;
var factorInfo = db.comatrix.findOne( {_id : key} )
if (factorInfo && factorInfo.value) {
db.user.find().forEach(function(in3){
var user_id = in3._id
var get = db.rec.findOne({user: in3._id, item: in2._id})
if (get)
db.user_prefer.insert( {user: in3._id, item: in1._id, value: factorInfo.value * get.rate} )
})
}
})
})

> db.recommendation.drop()
> db.user_prefer.aggregate([{$group: {_id: { user: "$user", item:"$item"}, value: {$sum: "$value"}}}, {$sort: {_id: 1}}]).forEach(function(input){
db.recommendation.insert({_id: input._id, user: input._id.user, item: input._id.item, value: input.value})
})
> db.recommendation.find()
{ "_id" : { "user" : "a", "item" : "1" }, "user" : "a", "item" : "1", "value" : 1.7000000000000002 }
{ "_id" : { "user" : "a", "item" : "2" }, "user" : "a", "item" : "2", "value" : 0.5 }
{ "_id" : { "user" : "a", "item" : "3" }, "user" : "a", "item" : "3", "value" : 3.6 }
{ "_id" : { "user" : "a", "item" : "4" }, "user" : "a", "item" : "4", "value" : 3.1 }
{ "_id" : { "user" : "a", "item" : "5" }, "user" : "a", "item" : "5", "value" : 0.5 }
{ "_id" : { "user" : "b", "item" : "1" }, "user" : "b", "item" : "1", "value" : 0.6 }
{ "_id" : { "user" : "b", "item" : "2" }, "user" : "b", "item" : "2", "value" : 0.8999999999999999 }
{ "_id" : { "user" : "b", "item" : "3" }, "user" : "b", "item" : "3", "value" : 2.1 }
{ "_id" : { "user" : "b", "item" : "4" }, "user" : "b", "item" : "4", "value" : 1.2 }
{ "_id" : { "user" : "b", "item" : "5" }, "user" : "b", "item" : "5", "value" : 0.8999999999999999 }
{ "_id" : { "user" : "c", "item" : "1" }, "user" : "c", "item" : "1", "value" : 0.8999999999999999 }
{ "_id" : { "user" : "c", "item" : "2" }, "user" : "c", "item" : "2", "value" : 0.2 }
{ "_id" : { "user" : "c", "item" : "3" }, "user" : "c", "item" : "3", "value" : 2 }
{ "_id" : { "user" : "c", "item" : "4" }, "user" : "c", "item" : "4", "value" : 1.7999999999999998 }
{ "_id" : { "user" : "c", "item" : "5" }, "user" : "c", "item" : "5", "value" : 0.2 }


清掉 user 已擁有 items:

> db.rec.find().forEach(function(input){
var key = { "user": input.user, "item": input.item }
db.recommendation.remove( { _id : { $eq: key } } )
})

> db.recommendation.find()
{ "_id" : { "user" : "a", "item" : "2" }, "user" : "a", "item" : "2", "value" : 0.5 }
{ "_id" : { "user" : "a", "item" : "5" }, "user" : "a", "item" : "5", "value" : 0.5 }
{ "_id" : { "user" : "b", "item" : "1" }, "user" : "b", "item" : "1", "value" : 0.6 }
{ "_id" : { "user" : "b", "item" : "4" }, "user" : "b", "item" : "4", "value" : 1.2 }
{ "_id" : { "user" : "c", "item" : "1" }, "user" : "c", "item" : "1", "value" : 0.8999999999999999 }
{ "_id" : { "user" : "c", "item" : "2" }, "user" : "c", "item" : "2", "value" : 0.2 }
{ "_id" : { "user" : "c", "item" : "5" }, "user" : "c", "item" : "5", "value" : 0.2 }


收工!

2014年10月24日 星期五

[Linux] Docker 使用筆記 - 常用指令 @ Ubuntu 14.04

搜尋可用 Images (此例關鍵字為 Ubuntu):

$ sudo docker search ubuntu

下載或更新指定 Image (此例為 ubuntu 14.04):

$ sudo docker pull ubuntu:14.04

列出所有 Images:

$ sudo docker images

刪除特定 Image:

$ sudo docker rmi IMAGE_ID

得知 container 的資訊或指定欄位(IPAddress、HostPort為例):

$ sudo docker inspect CONTAINER_ID_OR_NAME
$ sudo docker inspect --format '{{.NetworkSettings.IPAddress}}' CONTAINER_ID_OR_NAME
$ sudo docker inspect CONTAINER_ID_OR_NAME | grep "HostPort" | uniq | awk -F'"' '{print $4}'


停止所有 container:

$ sudo docker stop $(sudo docker ps -q)

刪除所有 container:

$ sudo docker rm $(sudo docker ps -a -q)

刪除所有 image:

$ sudo docker rmi $(sudo docker images -q)

刪除 untagged images:

$ sudo docker rmi $(sudo docker images -qf "dangling=true")

使用 nscenter 進入 container:

$ sudo docker inspect --format '{{.State.Pid}}' CONTAINER_ID
PID_NUMBER
$ sudo nsenter --target PID_NUMBER --mount --uts --ipc --net --pid

[Linux] Docker 使用筆記 - 透過 nsenter 進入當前 container 操作環境 @ Ubuntu 14.04

早期的 Docker 可以直接用 lxc-attach 取得 container 的 terminal 環境:

$ sudo docker ps --no-trunc
$ sudo lxc-attach -n CONTAINER_ID


但新版的只會噴 lxc-attach: failed to get the init pid 訊息,所以可以改用 nsenter 來操作吧!

$ sudo docker version
Client version: 1.0.1
Client API version: 1.12
Go version (client): go1.2.1
Git commit (client): 990021a
Server version: 1.0.1
Server API version: 1.12
Go version (server): go1.2.1
Git commit (server): 990021a

$ curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz | tar -zxf-
$ cd util-linux-2.24
$ ./configure --without-ncurses
$ make nsenter
$ sudo cp nsenter /usr/local/bin

$ sudo docker inspect --format "{{ .State.Pid }}" CONTAINER_ID_OR_NAME
$ sudo nsenter --target $PID --mount --uts --ipc --net --pid
root@##############:/#

2014年10月1日 星期三

iOS 開發筆記 - -[######## _viewControllerForSupportedInterfaceOrientationsWithDismissCheck:]: unrecognized selector sent to instance 0x########'

這幾晚和此 crash log 對抗了一下 Orz 這是一個在 iOS 8 才出現,但在 iOS 7 可以活得好好的 bug ,簡介一下狀況:

  • Project 採用 SingleView 建立,簡稱 ViewController1
  • 在 ViewController1 中,動態建立 UINavigationController ,並 push ViewController2 後,再透過 presentViewController 呈現,也就是在 ViewController1 沒有 UINavigationController 但 ViewController2 有。
  • 在 ViewController2 中,此 shouldAutorotate 皆回傳 YES

接著,只要在 ViewController2 中,透過 dismiss 返回到 ViewController1 時,再進行 device rotation 後,這時就會看到 crash log。但一樣的 code 擺在 iOS 7 卻能夠正常運作,並且可透過時間差進行 device rotation 觀看出發動 _viewControllerForSupportedInterfaceOrientationsWithDismissCheck 的物件,並非固定:

  • -[__NSCFString _viewControllerForSupportedInterfaceOrientationsWithDismissCheck:]: unrecognized selector sent to instance 0x########
  • -[UIStatusBarStyleRequest _viewControllerForSupportedInterfaceOrientationsWithDismissCheck:]: unrecognized selector sent to instance 0x########
  • -[__NSBlockVariable__ _viewControllerForSupportedInterfaceOrientationsWithDismissCheck:]: unrecognized selector sent to instance 0x########
  • ...

解法?就是從 ViewController1 就開始用 UINavigationController 吧!接著都走 UINavigationController 控制方式,例如 push、pop 後,即可避開這奇妙的問題。

2014年9月28日 星期日

iOS 開發筆記 - iPhone Simulator Document Path

開發 iOS app 時,若有資料儲存時,常常會想直接去翻 iPhone Simulator 的真實儲存位置,久了就記住位置:

$ ls ~/Library/Application\ Support/iPhone\ Simulator/7.1/Applications/

最近改用 iOS 8 時,卻找不到 8.0 位置 XD 直到在 App 中印出位置才得知換了,粗略位置:

$ ls ~/Library/Developer/CoreSimulator/Devices/########-####-####-####-###########/data/Containers/Data/Application/

2014年9月22日 星期一

[OSX] 找尋 ISO 639 和 ISO 3166 定義的 language list @ Mac OS X 10.9.5

想說翻一下 wiki 又有點 ooxx... 偷問一下高手,高手非常直觀地跟我說,用一下 /usr/share/locale !

由於我是用 Mac OS X ,該產品語言已經做得很不錯了!果真的輕輕鬆鬆找出我要的東西:

$ ls /usr/share/locale | grep -v "\\." | grep "_"
af_ZA
am_ET
be_BY
bg_BG
ca_ES
cs_CZ
da_DK
de_AT
de_CH
de_DE
el_GR
en_AU
en_CA
en_GB
en_IE
en_NZ
en_US
es_ES
et_EE
eu_ES
fi_FI
fr_BE
fr_CA
fr_CH
fr_FR
he_IL
hr_HR
hu_HU
hy_AM
is_IS
it_CH
it_IT
ja_JP
kk_KZ
ko_KR
lt_LT
nl_BE
nl_NL
no_NO
pl_PL
pt_BR
pt_PT
ro_RO
ru_RU
sk_SK
sl_SI
sr_YU
sv_SE
tr_TR
uk_UA
zh_CN
zh_HK
zh_TW


高手獻技:

$ cd /usr/share/locale && echo ??_??
af_ZA am_ET be_BY bg_BG ca_ES cs_CZ da_DK de_AT de_CH de_DE el_GR en_AU en_CA en_GB en_IE en_NZ en_US es_ES et_EE eu_ES fi_FI fr_BE fr_CA fr_CH fr_FR he_IL hr_HR hu_HU hy_AM is_IS it_CH it_IT ja_JP kk_KZ ko_KR lt_LT nl_BE nl_NL no_NO pl_PL pt_BR pt_PT ro_RO ru_RU sk_SK sl_SI sr_YU sv_SE tr_TR uk_UA zh_CN zh_HK zh_TW


後來又找到對應表: http://www.localeplanet.com/icu/

<?php
// http://www.localeplanet.com/icu/
$lang_map = array(
        'af_ZA' => 'Afrikaans (Suid-Afrika)',
        'am_ET' => 'አማርኛ (ኢትዮጵያ)' ,
        'be_BY' => 'беларуская (Беларусь)' ,
        'bg_BG' => 'български (България)',
        'ca_ES' => 'català (Espanya)',
        'cs_CZ' => 'čeština (Česká republika)',
        'da_DK' => 'dansk (Danmark)',
        'de_AT' => 'Deutsch (Österreich)',
        'de_CH' => 'Deutsch (Schweiz)',
        'de_DE' => 'Deutsch (Deutschland)',
        'el_GR' => 'Ελληνικά (Ελλάδα)',
        'en_AU' => 'English (Australia)',
        'en_CA' => 'English (Canada)',
        'en_GB' => 'English (United Kingdom)',
        'en_IE' => 'English (Ireland)',
        'en_NZ' => 'English (New Zealand)',
        'en_US' => 'English (United States)',
        'es_ES' => 'español (España)',
        'et_EE' => 'eesti (Eesti)' ,
        'eu_ES' => 'euskara (Espainia)',
        'fi_FI' => 'suomi (Suomi)',
        'fr_BE' => 'français (Belgique)',
        'fr_CA' => 'français (Canada)',
        'fr_CH' => 'français (Suisse)',
        'fr_FR' => 'français (France)',
        'he_IL' => 'עברית (ישראל)',
        'hr_HR' => 'hrvatski (Hrvatska)',
        'hu_HU' => 'magyar (Magyarország)',
        'hy_AM' => 'Հայերէն (Հայաստանի Հանրապետութիւն)',
        'is_IS' => 'íslenska (Ísland)',
        'it_CH' => 'italiano (Svizzera)' ,
        'it_IT' => 'italiano (Italia)',
        'ja_JP' => '日本語(日本)',
        'kk_KZ' => 'kk_KZ',
        'ko_KR' => '한국어(대한민국)',
        'lt_LT' => 'lietuvių (Lietuva)',
        'nl_BE' => 'Nederlands (België)',
        'nl_NL' => 'Nederlands (Nederland)',
        'no_NO' => 'no_NO',
        'pl_PL' => 'polski (Polska)',
        'pt_BR' => 'português (Brasil)',
        'pt_PT' => 'português (Portugal)',
        'ro_RO' => 'română (România)',
        'ru_RU' => 'русский (Россия)',
        'sk_SK' => 'slovenčina (Slovenská republika)',
        'sl_SI' => 'slovenščina (Slovenija)',
        'sr_YU' => 'sr_YU',
        'sv_SE' => 'svenska (Sverige)',
        'tr_TR' => 'Türkçe (Türkiye)',
        'uk_UA' => 'українська (Україна)',
        'zh_CN' => '中文(简体中文、中国)',
        'zh_HK' => '中文(繁體中文,中華人民共和國香港特別行政區)',
        'zh_TW' => '中文(繁體中文,台灣)'
);

2014年9月19日 星期五

[OSX] 使用 App Store 更新 Xcode 發生錯誤 @ Mac OS X 10.9.5, MBA


主因也有可能是空間太少了 :P
除了準備空間外,可以試看看 App Store Debug 界面:

$ defaults write com.apple.appstore ShowDebugMenu -bool true

接著重新打開 App Store 後,上方最右邊有 Debug 選項 -> Reset Application,大概多做幾次即可。如果很不幸的一直不幸,最終解法就是反安裝後,再下載。

2014年9月18日 星期四

iOS 開發筆記 - 驗證 Apple Push Notification PEM File 以及 Remove PEM Password

半年前曾開發過: 使用 Apple Push Notification service (APNs),最近更新 PEM  後,要驗證一下 PEM 是否正確。

驗證還滿簡單的,單純用 openssl 進行,假設是 dev 模式,對象就是 snadbox:

$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert cert.pem -key key.pem

其中 cert.pem 跟 key.pem 也可以合在一起:

$ cat cert.pem key.pem > out.pem
$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert out.pem


假設產出的 key.pem 有密碼保護,為了圖方便想要去密碼的話,可以用:

$ openssl rsa -in key.pem -out nopass.pem

如此一來就搞定了,若測試的是 Production PEM ,記得改用 gateway.push.apple.com:2195 即可。

$ openssl s_client -connect gateway.push.apple.com:2195 -cert cert.pem -key key.pem

2014年9月12日 星期五

[Python] 使用 Apache Web Server Access.log 把玩 CartoDB 視覺化地圖 @ Ubuntu 12.04



只要資料有 Geolocation,就能夠把玩 CartoDB 了 :P 若可以的話,再加上時間就更完美了。因此,最容易拿到的測資是 Apache web server log,把 access.log 挑點東西出來即可,至於 Geolocation 就用 ip 反查吧!

從 access.log 取出 ip list:

$ grep -v "^localhost\|::1" /var/log/apache2/access.log | awk '{print $1}' | uniq

首先,先到 Maxmind 下載最新的 GeoLite2-City 資訊:

$ wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
$ gunzip GeoLite2-City.mmdb.gz


安裝 geoip-bin 工具:

$ sudo apt-get install geoip-bin
$ geoiplookup 8.8.8.8
GeoIP Country Edition: US, United States

$ geoiplookup -f GeoLite2-City.mmdb 8.8.8.8
Error Traversing Database for ipnum = 134744072 - Perhaps database is corrupt?
Segmentation fault (core dumped)


囧...只好裝一下新版 maxmind python sdk 寫一段小 code:

$ sudo pip install geoip2

$ vim t.py
mport sys
import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
try:
        response = reader.city(sys.argv[1])
        print str(response.location.latitude)+","+str(response.location.longitude)
except Exception, e:
        pass
$ python t.py 8.8.8.8
37.386,-122.0838


接著,就乾脆寫 python 來處理 access.log 吧 XDD 用 command line 好像太冗長了。

$ sudo cp /var/log/apache2/access.log /tmp/access.log
$ sudo chmod 644 /tmp/access.log
$ vim log.py
import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
try:
        log = open('/tmp/access.log','rb').read()
        for rec in log.split('\n'):
                fields = rec.split(' ')
                try:
                        if fields[0] == 'localhost' or fields[0] == '::1' :
                                continue
                        response = reader.city(fields[0])
                        print fields[3][1:]+","+str(response.location.latitude)+","+str(response.location.longitude)
                except Exception, e:
                        pass
except Exception, e:
        pass

$ python log.py > log.csv
$ cat log.csv
...
07/Sep/2014:22:12:43,35.685,139.7514
07/Sep/2014:22:13:53,39.4899,-74.4773
...


對於時間格式不用擔心,直接丟進 cartodb.com 請他幫你處理!




匯入後,預設都是 string,可以把 field1 設成 date type,field2 跟 field3 都設成 number type,弄完順便 rename 一下,接著再點選 geo 欄位,可以採用 field2 跟 field3 來生成,如此就完成 CartoDB  table 製作。








最後再去視覺化那邊,挑一下以 date 為基準的時間變化,就可以有不錯的視覺圖表。

2014年9月11日 星期四

Android 開發筆記 - 透過 gsutil 取得 Google Play App Customer Reviews

依照官方文件簡介,對於 Android app 的評論,系統都會儲存在 Google Cloud Storage 裡頭,需要透過 gsutil 取出來使用,在此透過 pip 安裝 gsutil:

$ sudo pip install gsutil

使用前,需要先到 Google Play Developer Console -> Your android app -> 評分與評論 -> 底下最下方找到 ID,如 pubsite_prod_rev_0123456789。

接著,先設定 gsutil 存取權限:

$ gsutil config
This command will create a boto config file at /home/username/.boto
containing your credentials, based on your responses to the following
questions.
Please navigate your browser to the following URL:
https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&client_id=############.apps.googleusercontent.com&access_type=offline
In your browser you should see a page that requests you to authorize access to Google Cloud Platform APIs and Services on your behalf. After you approve, an authorization code will be displayed.

Enter the authorization code:


並透過瀏覽器得到 authorization code,輸入完後,會在詢問 Project ID,這時請記得填寫類似這串 pubsite_prod_rev_0123456789 的 ID。

之後,就可以透過 gsutil cp -r gs://pubsite_prod_rev_0123456789/reviews/reviews* 取得所有評論。

$ gsutil cp -r gs://pubsite_prod_rev_0123456789/reviews/reviews_* android_review/

這些 reviews 都是 CSV 格式,且第一列有顯示欄位資訊:

Package Name,App Version,Reviewer Language,Reviewer Hardware Model,Review Submit Date and Time,Review Submit Millis Since Epoch,Review Last Update Date and Time,Review Last Update Millis Since Epoch,Star Rating,Review Title,Review Text,Developer Reply Date and Time,Developer Reply Millis Since Epoch,Developer Reply Text,Review Link

看來只要寫隻簡易的 tool 就可以處理完畢囉:

$ file reviews_*.csv
reviews_*.csv: Little-endian UTF-16 Unicode text, with very long lines


因此,寫 php script 的話,可以先用 iconv 轉 UTF-8 後,再搭配 str_getcsv 處理:

<?php
$raw = file_get_contents(...);
$result = iconv($in_charset = 'UTF-16LE' , $out_charset = 'UTF-8' , $raw);
if( false !== $result )
$raw = $result;

$raw_lines = explode("\n", $raw);
array_shift($raw_lines); // title
foreach( $raw_lines as $line ) {
$fields = str_getcsv($line);
print_r($fields);
}

iOS 開發筆記 - 透過 iTunesConnect RSS 處理 App Customer Reviews 資料

以 Facebook 而言,可以輕易得知 App ID 為 284882215,而 iTunesConnect 有提供 RSS 方式查詢 Customer Reviews:

https://itunes.apple.com/us/rss/customerreviews/id=284882215/sortBy=mostRecent/xml

其中,上述有三個主要可變動的參數:country(us), app id (284882215), format(xml)

最近比較愛 json 的:

https://itunes.apple.com/us/rss/customerreviews/id=284882215/sortBy=mostRecent/json

想要台灣區評價:

https://itunes.apple.com/tw/rss/customerreviews/id=284882215/sortBy=mostRecent/json

問題應該就是 country 到底有哪些,可以用既定資料每個都抓一遍啦。

對於格式有興趣的可以用得到可讀性較佳的資料格式:

$ curl https://itunes.apple.com/tw/rss/customerreviews/id=284882215/sortBy=mostRecent/json | python -mjson.tool

若仔細看的話,還可以看到 User ID 、追蹤到 User 對其他款 app 評價等,此外,也有 first page 跟 last page 存取方式:

https://itunes.apple.com/tw/rss/customerreviews/page=1/id=284882215/sortby=mostrecent/json
https://itunes.apple.com/tw/rss/customerreviews/page=10/id=284882215/sortby=mostrecent/json

最大頁數只有到 10 而已(CustomerReviews RSS page depth is limited to 10)

至於要用 json 還是 xml 好?目前的心得是... json 會缺少 user comment updated 資訊,而 xml 卻也會碰到 format error 的情況 Orz 只能...看著辦了 XD


以 PHP 處理為例:

$ cat test.php
<?php
$app_id = '284882215';
$country_list = array( 'tw', 'us');
$format = 'xml';
$page_list = array(1,2,3,4,5,6,7,8,9,10);
foreach( $country_list as $country ) {
        foreach( $page_list as $page ) {
                $url = "https://itunes.apple.com/$country/rss/customerreviews/id=$app_id/page=$page/$format";
                if ($format == 'json') {
                        $raw = json_decode(@file_get_contents($url), true);
                        //print_r($raw);
                        for ($i=1, $cnt=count($raw['feed']['entry']) ; $i<$cnt ; ++$i) {
                                print_r(array(
                                        'user_id' => $raw['feed']['entry'][$i]['id']['label'],
                                        'user_name' => $raw['feed']['entry'][$i]['author']['name']['label'],
                                        'user_uri' => $raw['feed']['entry'][$i]['author']['uri']['label'],
                                        'title' => $raw['feed']['entry'][$i]['title']['label'],
                                        'content' => $raw['feed']['entry'][$i]['content']['label'],
                                        'version' => $raw['feed']['entry'][$i]['im:version']['label'],
                                        'rating' => $raw['feed']['entry'][$i]['im:rating']['label'],
                                ));
                        }
                } else {
                        $raw = simplexml_load_string(@file_get_contents($url));
                        //print_r($raw);
                        for($i=1, $cnt = count($raw->entry); $i<$cnt ; ++$i) {
                                //print_r($raw->entry[$i]);
                                $imAttrs = $raw->entry[$i]->children('im', true);
                                //print_r($imAttrs);
                                print_r(array(
                                        'date' => (string)$raw->entry[$i]->updated,
                                        'user_id' => (string)$raw->entry[$i]->id,
                                        'user_name' => (string)$raw->entry[$i]->author->name,
                                        'user_uri' => (string)$raw->entry[$i]->author->uri,
                                        'title' => (string)$raw->entry[$i]->title,
                                        'content' => (string)$raw->entry[$i]->content[0],
                                        'version' => (string)$imAttrs->version,
                                        'rate' => (string)$imAttrs->rating,
                                ));
                        }
                }
                exit;
        }
}

$ php test.php
Array
(
    [date] => 2014-09-10T09:10:00-07:00
    [user_id] => ###########
    [user_name] => ###########
    [user_uri] => https://itunes.apple.com/tw/reviews/id###########
    [title] => 有Bug請改善
    [content] => 點朋友的動態跑不出來!!
    [version] => 14.0
    [rating] => 1
)
Array
(
    [date] => 2014-09-10T09:03:00-07:00
    [user_id] => ###########
    [user_name] => ###########
    [user_uri] => https://itunes.apple.com/tw/reviews/id###########
    [title] => 一直自動關掉
    [content] => 用一用會一直自動跳掉,很頻繁,很煩。
    [version] => 14.0
    [rating] => 1

)

2014年9月10日 星期三

[SQL] SELECT IN SELECT 以及 Pagination 的使用 @ MySQL 5.6

使用 SQL 語法時,有時會需要從另一張 Table 取出清單,接著對清單內的資料做為基準再進行一次資料的擷取,直觀的想法大概是 SELECT something FROM Table1 WHERE id IN (SELECT id FROM Table2 WHERE ... )。

可惜上述語法是不行的 XD 要改成 JOIN 的做法:

SELECT something FROM Table1, (SELECT id FROM Table2) AS list WHERE Table1.id = list.id;

接著,偶爾會需要 pagination 的需求,加個 LIMIT 的用法,這時候又會想要回報全部有幾筆資料(對於一些搜尋引擎的設計,有些是採用預估的方式),以便前端可以估算有幾筆資料。

最簡單的解法是再用一個 SQL Query 去問 Table2 的 id 資料,但想要更快一點,就來試試 MySQL User-Defined Variables 吧!

SELECT something, @n AS total FROM Table1, (SELECT id, CASE WHEN @n > 0 THEN @n := @n + 1 ELSE @n := 1 END AS n FROM Table2, (SELECT @n := 0) AS init) AS list WHERE Table1.id = list.id;

如此一來,結果都會有個 total 筆數跟著,雖然仍不夠好,但也不錯啦 XD  而搭配 LIMIT OFFSET,COUNT 時,total 的資訊是來自掃 Table2 的資料,所以也能正常顯示:

SELECT something, @n AS total FROM Table1, (SELECT id, CASE WHEN @n > 0 THEN @n := @n + 1 ELSE @n := 1 END AS n FROM Table2, (SELECT @n := 0) AS init) AS list WHERE Table1.id = list.id LIMIT 0,10;

[C++] 使用 PCRE、RE2 進行 Match all (如同 PHP preg_match_all 效果) @ Ubuntu 14.04

對 PHP 來說:

$ vim t.php
<?php
$data = "..<a href='...'>...</a>.."; //file_get_contents(...);
if (preg_match_all("#<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>#", $data, $matches) )
        print_r($matches);
$ php t.php
Array
(
    [0] => Array
        (
            [0] => <a href='...'>...</a>
        )

    [1] => Array
        (
            [0] => ...
        )

    [2] => Array
        (
            [0] => ...
        )

)


對 PCRE 來說:

$ vim pcre_test.cpp
#include <pcre.h>
#include <iostream>

int main() {

const char *error;
int erroroffset;
pcre *preg_pattern_a_tag = pcre_compile("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>", PCRE_MULTILINE, &error,  &erroroffset, NULL);

if (!preg_pattern_a_tag) {
std::cout << "ERROR\n";
return -1;
}

std::string raw = "..<a href='...'>...</a>..";

unsigned int offset = 0;
unsigned int len = raw.size();
int matchInfo[3*2] = {0};
int rc = 0;

while (offset < len && (rc = pcre_exec(preg_pattern_a_tag, 0, raw.c_str(), len, offset, 0,  matchInfo, sizeof(matchInfo))) >= 0) {
for (int n=0; n<rc ; ++n) {
int data_length = matchInfo[2*n+1] - matchInfo[2*n];
std::cout << "Found:[" << raw.substr(matchInfo[2*n], data_length) << "]\n";
}
offset = matchInfo[1];
}
return 0;
}
$ g++ -std=c++11 pcre_test.cpp -lpcre
$ ./a.out
Found:[<a href='...'>...</a>]
Found:[...]
Found:[...]


對 RE2 來說:

$ vim re2_test.cpp
#include <re2/re2.h>
#include <iostream>

int main() {

//RE2 preg_pattern_a_tag("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>", RE2::Latin1);
RE2 preg_pattern_a_tag("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>");

std::string raw = "..<a href='...'>...</a>..";

re2::StringPiece result_a_href, result_a_body;

while(RE2::PartialMatch(raw, preg_pattern_a_tag, &result_a_href, &result_a_body)) {
std::cout << "result_a_href:[" << result_a_href << "]\n";
std::cout << "result_a_body:[" << result_a_body << "]\n";
raw = result_a_body.data();
}
return 0;
}
$ g++ -std=c++11 re2_test.cpp /path/libre2.a -lpthread
$ ./a.out
result_a_href:[...]
result_a_body:[...]

2014年9月3日 星期三

AWS 筆記 - 使用 Amazon Route53 設定 DNS SPF Record

如果說 MX 是用來提供寄件者得知 mail server 在哪邊,那 SPF 則是提供 mail receiver 驗證寄件者使用的 mail server 是不是接近已知、合法的 server。因此,設定 DNS SPF Record 是會降低信件誤判成垃圾郵件的機率。

採用 Amazon Route53 時,設定 TXT Record (有 SPF Record,但這次沒試)

"v=spf1 a mx a:YourServerHostname  ~all"

最後面的 ~all 代表上述未定義的,可能是錯的。若要強制歸類在錯誤,就用 -all 即可。

驗證方式(此例以 cs.nctu.edu.tw 為例):

$ nslookup -q=txt cs.nctu.edu.tw
...
cs.nctu.edu.tw  text = "v=spf1 a mx a:farewell.cs.nctu.edu.tw a:csmailer.cs.nctu.edu.tw a:tcsmailer.cs.nctu.edu.tw a:csmailgate.cs.nctu.edu.tw a:csmail1.cs.nctu.edu.tw a:csmail2.cs.nctu.edu.tw a:csws1.cs.nctu.edu.tw a:csws2.cs.nctu.edu.tw ~all"
...


可以看到 "v=spf1 ... " 資訊,代表 DNS TXT Record 設定無誤,記得這是有 dns cache 機制,不見得馬上更改或是馬上可以查詢到。

接著,可以用 Gmail 來測試,從自家 mail server 寄信到 Gmail,若沒有設定 DNS SPF Record ,會顯示:

Received-SPF: none (google.com: YourSender@YourMailServer does not designate permitted sender hosts) client-ip=YourMailServerIP;

若有設定 DNS SPF Record 但不在名單內,此例為 ~all 用法:

Received-SPF: softfail (google.com: domain of transitioning YourSender@YourMailServer does not designate YourMailServerIP as permitted sender) client-ip=YourMailServerIP;

若設定正確即在 DNS SPF Record 定義內:

Received-SPF: pass (google.com: domain of YourSender@YourMailServer designates YourMailServerIP as permitted sender) client-ip=YourMailServerIP;

要留意,如果租的機器是在一些常見的 VPS 時,對外走的可能是 IPv6 ,此時記得 DNS Reocrd 中,也要用 AAAA Record (IPv6) 定義。

2014年9月2日 星期二

[Linux] 透過 alias 與 virtual 機制建立 no-reply @ hostname 帳號 @ Postfix 2.9.6, Ubuntu 12.04

新增一筆 alias 用來導向 /dev/null

$ grep alias_maps /etc/postfix/main.cf
alias_maps = hash:/etc/aliase
$ sudo vim /etc/aliases
...
devnull: /dev/null
...


採用 virtual account 新增 no-reply 帳號:

$ grep virtual_alias_maps /etc/postfix/main.cf
$ sudo vim /etc/postfix/main.cf
virtual_alias_maps = hash:/etc/postfix/virtual

$ sudo vim /etc/postfix/virtual
no-reply@hostname devnull

$ sudo newaliases
$ sudo postmap /etc/postfix/virtual
$ sudo postfix reload
postfix/postfix-script: refreshing the Postfix mail system


至於測試方式,就直接連他寄信看看:

$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 hostname ESMTP Postfix (Ubuntu)
HELO localhost
250 hostname
mail from:<root@localhost>
250 2.1.0 Ok
rcpt to:<no-reply@hostname>
250 2.1.0 Ok
quit
221 2.0.0 Bye
Connection closed by foreign host.


如果出現 451 4.3.0 <no-reply@hostname>: Temporary lookup failure ,請記得翻一下 /var/log/mail.err ,若是單純 error: open database /etc/postfix/virtual.db: No such file or directory 問題,記得跑一下 sudo postmap /etc/postfix/virtual 即可。

2014年8月28日 星期四

[Python] Google App Engines 發送 POST 與 JSON-RPC

初始資料:

url = 'http://server/cgi'
data = {
"arg1" : "val1",
"arg2" : "val2"
}


發送 POST:

import urllib
from google.appengine.api import urlfetch
post_data = urllib.urlencode(data)
response = urlfetch.fetch(url=url,payload=post_data,method=urlfetch.POST,headers={'Content-Type': 'application/x-www-form-urlencoded'})


發送 JSONRPC:

import urllib2
headers = {
"Content-Type": "application/json"
}
post_data = json.dumps(data)
req = urllib2.Request(url, data, headers)
response = urllib2.urlopen(req).read()

2014年8月27日 星期三

[PHP] Mantis Plugin 開發筆記 @ Mantis 1.2.17

由於公司管理者熟悉 Mantis 系統,最近挑了 Mantis 來做類似 Customer Relationship Management (CRM) 服務,接著很容易碰到 User Group 的需求,這是在系統本身不存在的功能。

在網路上找了許久,有發現 User Group 相關的 Plugin,但是拿出來搭配最新版 Mantis 1.2.17 時,出現很多 Link 失效、或是某些表單送出時,導向回來失敗等。由於 trace/fix 完就等於把整個 plugin 做完了,就順便把 Mantis Plugin 開發筆記起來 Orz

首先,在 Mantis 架構下,有提供 Plugin 開發機制,提供在系統任何一個端點加入 Event Notification 架構,透過這個 notification 導入 Plugin 程式碼,以 User Group 來說,就是管理者在編輯使用者帳號時,希望也有張表可以查看目前使用者歸屬的 User Group Management,以及簡易的編輯、刪除等。

其中 Mantis Event 架構可以查詢:mantis/core/event_api.php,而 Mantis Plugin 架構可參考:mantis/core/plugin_api.php。開發 Mantis Plugin 文件,請參考這份:Building a Plugin,看完大概也了解了。

整體流程:
  1. 寫個 class ExamplePlugin extends MantisPlugin 
  2. 定義 plugin 需要的 db schema
  3. 綁定 event 串接自己定義的 function
其他心得:
  • 若 plugin db schema 想要變更時,除了刪除既定存在的外,還需要去刪除 mantis_config_table 裡頭的記錄,例如刪掉 config_id 等價於自己 plugin 名稱才行。如此一來,plugin 重新安裝時,才會重新建立對應的 table

2014年8月25日 星期一

[PHP] Mantis Email Notification Settings @ Ubuntu 12.04

在 Ubuntu 12.04 透過 apt-get 安裝後,配置好系統管理後,又手動把它生成 Mantis 1.2.17 了。預設 Mantis 是有啟動 Email Notification,包含新增帳號後,還要由對方 email 去設定密碼等。對於 Email Notification 的設定,如事件通知的寄信者資訊等,都是直接更改 /path/mantis/config_inc.php。

可以參考 http://www.mantisbt.org/manual/admin.config.email.htmlhttp://www.mantisbt.org/manual/admin.customize.email.html,簡易筆記:

//$g_enable_email_notification = OFF;
$g_mail_receive_own = OFF;
$g_default_notify_flags = array(
'reporter' => ON,
'handler' => ON,
'monitor' => ON,
'bugnotes' => ON,
'threshold_min' => NOBODY,
'threshold_max' => NOBODY
);

//
// Select the method to mail by: PHPMAILER_METHOD_MAIL for use of mail() function, PHPMAILER_METHOD_SENDMAIL for sendmail (or postfix), PHPMAILER_METHOD_SMTP for SMTP. Default is PHPMAILER_METHOD_MAIL.
//
$g_phpMailer_method = PHPMAILER_METHOD_MAIL;
$g_smtp_host = 'localhost';
$g_smtp_username = '';
$g_smtp_password = '';
$g_administrator_email = 'admin@example.com';
$g_from_email = 'noreply@example.com';
$g_from_name = 'Mantis Bug Tracker';

2014年8月24日 星期日

iOS 開發筆記 - UICollectionView resize after device rotation

最近在把玩 UICollectionView 並著手處理 resize 的時機點,經 StackOverflow - Change UICollectionViewCell size on different device orientations 的討論,純 coding without storyboard 時,需處理兩件事:

  • - (CGSize)collectionView:layout:sizeForItemAtIndexPath:
  • - (void)didRotateFromInterfaceOrientation:

就像在 UITableViewController 時,告訴 ViewController 這個 UITableViewCell 到底有多高,只是在 UICollectionViewCell 時,還能定義有多寬;在 didRotateFromInterfaceOrientation 事件中,則是告訴 UIViewController 更新畫面。

只是單純上述兩者並無法改變已經存在的 UICollectionViewCell ,對於已經存在的,必須強制處理,簡言之,需要再多幾件事:

  • 在 - (void)didRotateFromInterfaceOrientation: 時,請記得重新定義 UICollectionView frame 資訊,並且使用 [yourCollectionView reloadData] 的方式重繪資料
  • 在 - (UICollectionViewCell *)collectionView:cellForItemAtIndexPath: 中,若每一個 UICollectionViewCell 並非有規則的,則需要動態偵測並重新設定 frame 資訊
如此一來,就能達到 auto resize 的效果了 Orz  若沒這麼龜毛,大概用一下 Storyboard 就解掉了 XD

2014年8月23日 星期六

iOS 開發筆記 - UITextAlignmentLeft/UITextAlignmentCenter/UITextAlignmentRight is deprecated first deprecated in ios 6.0

從 UITextAlignment* 改用 NSTextAlignment* 即可。

iOS 開發筆記 - Use UICollectionView without Storyboard and XIB


最近才發現這個 Class ,真是乃覺三十里 XD 有可能已經習慣自己刻 UITableViewCell 了 Orz 所以一直沒去學習新技能,這次看到一些特效後,原本以為單純手動安排 View 變化,仔細一看才發現 UICollectionView 啦,就順手筆記一下,此外,坊間多為使用 Storyboard、XIB 的做法。
  1. Create an ViewControler extends UIViewController
  2. Add UICollectionView *collectionView property
  3. Init collectionView with UICollectionViewFlowLayout
  4. Use UICollectionViewDelegate and UICollectionViewDataSource
  5. Implement collectionView:numberOfItemsInSection: and collectionView:cellForItemAtIndexPath:
如此一來,就可以動了 XD

//
// TestCollectionViewController.h:
//
#import <UIKit/UIKit.h>

@interface TestCollectionViewController : UIViewController
@property (nonatomic, strong) UICollectionView *collectionView;
@end


//
// TestCollectionViewController.m:
//
#import "TestCollectionViewController.h"

@interface TestCollectionViewController () <UICollectionViewDelegate, UICollectionViewDataSource>

@end

@implementation TestCollectionViewController

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 10;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell * collectionViewCell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    
        CGFloat comps[3];
        for (int i = 0; i < 3; i++)
            comps[i] = (CGFloat)arc4random_uniform(256)/255.f;
        collectionViewCell.backgroundColor = [UIColor colorWithRed:comps[0] green:comps[1] blue:comps[2] alpha:1.0];
    return collectionViewCell;
}

- (UICollectionView *)collectionView
{
    if (!_collectionView) {
        UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
        flowLayout.itemSize = CGSizeMake(100, 100);
        flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
     
        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) collectionViewLayout:flowLayout];
        _collectionView.delegate = self;
        _collectionView.dataSource = self;
        _collectionView.backgroundColor = [UIColor whiteColor];
        [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
    }
    return _collectionView;
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [self.view addSubview:self.collectionView];
 
    // Do any additional setup after loading the view.
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

2014年8月22日 星期五

[PHP] 擴充 Mantis SOAP API 、處理 DB Query 心得

前置準備:
  • soap api source code 就擺在 mantis/api/soap 中
  • 記得清掉 SoapServer 跟 SoapClient 的 cache 資料,可以用 ini_set("soap.wsdl_cache_enabled", 0); 或是在 new SoapServer 或 SoapClient 時,指定 'cache_wsdl' => WSDL_CACHE_NONE 資訊
預計新增一個 api 名為 mc_user_group ,需要做的動作:
  • 規劃 mc_user_group 的 input 跟 output 資訊,此例 input 為 username, password,而 output 是 ObjectRefArray,由於都是既定 type ,所以不需額外定義
  • 更新 WSDL 定義,可以從已存在的 mc_enum_access_levels api 來仿照
    • <message name="mc_enum_groupRequest">
      <part name="username" type="xsd:string" />
      <part name="password" type="xsd:string" /></message>
      <message name="mc_enum_groupResponse">
      <part name="return" type="tns:ObjectRefArray" /></message>
    • <operation name="mc_enum_group">
      <documentation>Get the enumeration for group</documentation>
      <input message="tns:mc_enum_groupRequest"/>
      <output message="tns:mc_enum_groupResponse"/>
      </operation>
    • <operation name="mc_enum_group">
      <soap:operation soapAction="http://www.mantisbt.org/bugs/api/soap/mantisconnect.php/mc_enum_group" style="rpc"/>
      <input><soap:body use="encoded" namespace="http://futureware.biz/mantisconnect" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/></input>
      <output><soap:body use="encoded" namespace="http://futureware.biz/mantisconnect" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/></output>
      </operation>
  • 實作 function mc_enum_group( $p_username, $p_password ),若此 function 定義在其他檔案內,記得要更新 mantis/api/soap/mc_core.php 即可
如此一來,就完成擴充 Mantis SOAP api 的任務了,記得要把 Server 跟 Client 的 SOAP Cache 都必須清掉才會正常,不然 client site 會看到類似的訊息(扣除還沒實作的情況):

PHP Fatal error:  Uncaught SoapFault exception: [SOAP-ENV:Server] Procedure 'your_func' not present

以下是片段程式碼:

// Client
$TARGET_API='https://host/mantis/api/soap/mantisconnect.php?wsdl';
ini_set("soap.wsdl_cache_enabled","0");
$c = new SoapClient($TARGET_API);

// debug
// print_r($c->__getFunctions ());

print_r( $c->mc_enum_group($user, $pass) );


// Server
        require_once( 'mc_core.php' );
        ini_set("soap.wsdl_cache_enabled","0");
        $server = new SoapServer("mantisconnect.wsdl",
                        array('features' => SOAP_USE_XSI_ARRAY_TYPE + SOAP_SINGLE_ELEMENT_ARRAYS)
        );
        $server->addFunction(SOAP_FUNCTIONS_ALL);
        $server->handle();


其他心得:

  • 自定 Mantis SOAP API 時,需要留意 input, output 的地方,假設 output 的是 ObjectRefArray 形態,那輸出只能有 id, name ,其他欄位都會被濾掉
  • Mantis SOAP API 使用上要留意權限問題,例如發 issue 用的帳號,若是 repoter 的等級,不能去做 assign monitors 的行為,會有權限問題,有問題時,可以試著用 mc_issue_add 跟 mc_issue_update 交叉測試。

最後,再補一個 DB 操作方式,output 仍以 ObjectRefArray 為例:

function mc_enum_group( $p_username, $p_password ) {
        $t_user_id = mci_check_login( $p_username, $p_password );
        if ( $t_user_id === false ) {
                return mci_soap_fault_login_failed();
        }
         
        $t_result = array();
         
        $query = "SELECT * FROM mantis_user_table";
     
        $result = db_query( $query );
        //file_put_contents('/tmp/debug', print_r($result, true) );

// 此例把 username 充當回傳的 name 資訊、user id 當作回傳 id 資訊
        for( $i=0, $cnt=db_num_rows($result) ; $i<$cnt; ++$i ) {
                $row = db_fetch_array( $result );
                $obj = new stdClass();
                $obj->id = $row['id'];
                $obj->name = $row['username'];
                array_push( $t_result, $obj );
        }    
        return $t_result;
}

2014年8月21日 星期四

[Linux] 安裝 nicstat @ Ubuntu 12.04

在 Ubuntu 14.04 只需用 atp-get install nicstat 即可,但在 12.04 就得自己編了:

$ sudo apt-get install gcc gcc-multilib
$ cd /tmp
$ wget -qO- http://nchc.dl.sourceforge.net/project/nicstat/nicstat-1.92.tar.gz | tar -xvzf -
$ cp /tmp/nicstat-1.92/Makefile.Linux /tmp/nicstat-1.92/Makefile
$ cd /tmp/nicstat-1.92 && make && sudo make install

[Linux] 使用常見的指令進行系統監控 @ Ubuntu 14.04

取得 Server 代號 - 使用 hostname 指令:

$ hostname

取得 Server OS 資訊 - 使用 lsb_release 指令:

$ lsb_release -a

取得 CPU 使用率 - 使用 vmstat、tail 和 awk 指令:

$ echo $((100-$(vmstat|tail -1|awk '{print $15}')))

取得 System Loading 資訊 - 使用 uptime 和 awk 指令:

$ uptime | egrep -o 'load average[s]*: [0-9,\. ]+' | awk -F',' '{print $1$2$3}' | awk -F' ' '{print $3}'
$ uptime | egrep -o 'load average[s]*: [0-9,\. ]+' | awk -F',' '{print $1$2$3}' | awk -F' ' '{print $4}'
$ uptime | egrep -o 'load average[s]*: [0-9,\. ]+' | awk -F',' '{print $1$2$3}' | awk -F' ' '{print $5}'


取得 Apache Web Server Processes 資訊 - 使用 pgrep 跟 wc 指令:

$ pgrep apache2 | wc -l

取得 Memory 使用率 - 使用 free、grep 跟 awk 指令:

$ free -m | grep Mem | awk '{print $3/$2 * 100}'

取得 Network 即時流量 - 使用 nicstat、grep 跟 awk,假定網路卡是 eth 開頭:

In:
$ nicstat | grep eth |  awk '{print $3}'

Out:
$ nicstat | grep eth |  awk '{print $4}'


最後,檢查上述指令是否都存在:

#!/bin/sh
CMD_USAGE=$(echo 'hostname curl pgrep wc awk tail uptime vmstat free nicstat' | tr ";" "\n")
for cmd in $CMD_USAGE
do
        path=`which $cmd`
        if [ -z $path ] || [ ! -x $path ] ; then
                echo "$cmd not found"
                exit
        fi
done


其他資源: