2013年9月11日 星期三

Google Doc - Excel 存取其他 sheets 欄位資料

Google Doc - excel

最近用 Google doc 製作表單給大家填寫,當別人回送表單時,系統會用 Google Excel 進行一筆筆的紀錄。

例如表單的目的是要統計活動參與人數,以及攜家帶眷或是搭車人數的統計等。由於原本回應表單 sheet 1 是一筆筆的進來,雖可以更改數值,但系統不允許自己修改欄位,這時就必須另外建立一個 sheet 2 來處理,就碰到跨 sheets 存取的問題。

所幸這個還是有解,工程師只要面對可以程式化的東西,就會不小心玩過頭 XD

總結一下,存取格式: '表單名字'!欄位代號
更多資訊請參考:Reference data from other sheets

其他常用 function:
  • 計算吃素人數 =COUNTIF('表單回應'!H:H, "素食"),其中 "表單回應" 之 H 欄位只有 "葷食" 跟 "素食" 選項
  • 計算累積人數 =SUM('表單回應'!C:C),其中 C 欄位是數字
  • 計算填表人數 =COUNTA('表單回應'!A:A)-1,代表計算共有幾筆,減一是減去第一列的標題列

2013年9月10日 星期二

做人如果沒夢想,那跟鹹魚有什麼分別?


今年周邊很多人都換了工作或正在嘗試轉型,數一數大概有10多人吧 XD

最近也看了幾部電影,例如「古魯家族/瘋狂原始人」、「海苔億萬富翁/The Billionaire」,還滿多跟人生方向進行相關的內容,但最後,決定的還是自己啊。

2013年8月31日 星期六

SQLite Full-Text Searches 與 Custom Tokenizers 筆記 (FTS3/FTS4)

雖然敝公司的強項就是做 search engines 跟資料儲存管理,但研究其他家的 search 機制也是很基本的功夫。SQLite 小歸小,卻也五臟俱全,應該稱得上市面用戶最廣的資料庫系統吧?光 browser 或 mobile phone 就打趴一大片。

SQLITE Full-text Search 官方介紹網址: SQLite FTS3 and FTS4 Extensions

簡言之 FTS4 比 FTS3 更有效率,但也更吃資料空間。要留意的版本資狀態:
FTS4 is an enhancement to FTS3. FTS3 has been available since SQLite version 3.5.0 in 2007-09-04. The enhancements for FTS4 were added with SQLite version 3.7.4 on 2010-12-08.
至於 FTS3/FTS4 使用時機:
FTS4 is sometimes significantly faster than FTS3, even orders of magnitude faster depending on the query, though in the common case the performance of the two modules is similar. FTS4 also offers the enhanced matchinfo() outputs which can be useful in ranking the results of a MATCH operation. On the other hand, in the absence of a matchinfo=fts3 directive FTS4 requires a little more disk space than FTS3, though only a percent of two in most cases.
建立 FTS3/FTS4 Tables:

-- Create an FTS table named "pages" with three columns:
CREATE VIRTUAL TABLE pages USING fts4(title, keywords, body);

-- Create an FTS table named "mail" with two columns. Datatypes
-- and column constraints are specified along with each column. These
-- are completely ignored by FTS and SQLite.
CREATE VIRTUAL TABLE mail USING fts3(
  subject VARCHAR(256) NOT NULL,
  body TEXT CHECK(length(body)<10240)
);


至於 FTS Queries 範例:

-- The examples in this block assume the following FTS table:
CREATE VIRTUAL TABLE mail USING fts3(subject, body);

SELECT * FROM mail WHERE rowid = 15;                -- Fast. Rowid lookup.
SELECT * FROM mail WHERE body MATCH 'sqlite';       -- Fast. Full-text query.
SELECT * FROM mail WHERE mail MATCH 'search';       -- Fast. Full-text query.
SELECT * FROM mail WHERE rowid BETWEEN 15 AND 20;   -- Slow. Linear scan.
SELECT * FROM mail WHERE subject = 'database';      -- Slow. Linear scan.
SELECT * FROM mail WHERE subject MATCH 'database';  -- Fast. Full-text query.


以及這段:

-- Example schema
CREATE VIRTUAL TABLE mail USING fts3(subject, body);

-- Example table population
INSERT INTO mail(docid, subject, body) VALUES(1, 'software feedback', 'found it too slow');
INSERT INTO mail(docid, subject, body) VALUES(2, 'software feedback', 'no feedback');
INSERT INTO mail(docid, subject, body) VALUES(3, 'slow lunch order',  'was a software problem');

-- Example queries
SELECT * FROM mail WHERE subject MATCH 'software';    -- Selects rows 1 and 2
SELECT * FROM mail WHERE body    MATCH 'feedback';    -- Selects row 2
SELECT * FROM mail WHERE mail    MATCH 'software';    -- Selects rows 1, 2 and 3
SELECT * FROM mail WHERE mail    MATCH 'slow';        -- Selects rows 1 and 3


接著,提提要使用 FTS3 / FTS4 的用法,須要在 compile sqlite3 加上設定才行,預設沒有啟用,而啟用 FTS3 也會啟用 FTS4:

-DSQLITE_ENABLE_FTS3
-DSQLITE_ENABLE_FTS3_PARENTHESIS




CPPFLAGS="-DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS" ./configure <configure options>

此外,切 token 預設可選的機制有 "simple", "porter" (Porter Stemming algorithm), "icu" (需要SQLITE_ENABLE_ICU, 要有C版ICU Library), 和 "unicode61" (SQLite version 3.7.13之後支援)。

-- Create an FTS table named "papers" with two columns that uses
-- the tokenizer "porter".
CREATE VIRTUAL TABLE papers USING fts3(author, document, tokenize=porter);

-- Create an FTS table with a single column - "content" - that uses
-- the "simple" tokenizer.
CREATE VIRTUAL TABLE data USING fts4(tokenize=simple);

-- Create an FTS table with two columns that uses the "icu" tokenizer.
-- The qualifier "en_AU" is passed to the tokenizer implementation
CREATE VIRTUAL TABLE names USING fts3(a, b, tokenize=icu en_AU);


其中 simple 就是會把資料都先轉成小寫,查詢時也會將 keyword 轉小寫;porter 則是支援相關字詞的搜尋,例如資料內是存 "frustrated" 字,但用 "Frustrated" 和 "Frustration” 都可以查到;icu library 全名則為 "International Components for Unicode" library;Unicode61 則跟 simple 很像,但支援 unicode,如 unicode 空白與鏢體符號(切 token 用的 delimiter)等,而 simple 只認得 ASCII 部分。

接著,進入正題 SQLite FTS 這迷人的架構就在於可自定 tokenizers 啦,也是這篇筆記的重點,Custom (User Implemented) Tokenizers,不過網站上只有片段程式碼,問了同事才知道接下來又要翻 code 才行 :P 於是建議我挑 simple_tokenizer 來看,在 sqlite3.c 中,可以搜尋 tokenizers 的描述:
The fts3 built-in tokenizers - "simple", "porter" and "icu"- are implemented in files fts3_tokenizer1.c, fts3_porter.c and fts3_icu.c respectively. The following three forward declarations are for functions declared in these files used to retrieve the respective implementations.
Calling sqlite3Fts3SimpleTokenizerModule() sets the value pointed to by the argument to point to the "simple" tokenizer implementation.
And so on.
故搜尋 fts3_tokenizer1.c 可得知以下的定義:

/*
** 2006 Oct 10
**
** The author disclaims copyright to this source code.  In place of
** a legal notice, here is a blessing:
**
**    May you do good and not evil.
**    May you find forgiveness for yourself and forgive others.
**    May you share freely, never taking more than you give.
**
******************************************************************************
**
** Implementation of the "simple" full-text-search tokenizer.
*/

typedef struct simple_tokenizer {
  sqlite3_tokenizer base;
  char delim[128];             /* flag ASCII delimiters */
} simple_tokenizer;

typedef struct simple_tokenizer_cursor {
  sqlite3_tokenizer_cursor base;
  const char *pInput;          /* input we are tokenizing */
  int nBytes;                  /* size of the input */
  int iOffset;                 /* current position in pInput */
  int iToken;                  /* index of next token to be returned */
  char *pToken;                /* storage for current token */
  int nTokenAllocated;         /* space allocated to zToken buffer */
} simple_tokenizer_cursor;

static int simpleDelim(simple_tokenizer *t, unsigned char c){
  return c<0x80 && t->delim[c];
}
static int fts3_isalnum(int x){
  return (x>='0' && x<='9') || (x>='A' && x<='Z') || (x>='a' && x<='z');
}

/*
** Create a new tokenizer instance.
*/
static int simpleCreate(
  int argc, const char * const *argv,
  sqlite3_tokenizer **ppTokenizer
){
// ...
}

/*
** Destroy a tokenizer
*/
static int simpleDestroy(sqlite3_tokenizer *pTokenizer){
  sqlite3_free(pTokenizer);
  return SQLITE_OK;
}

/*
** Prepare to begin tokenizing a particular string.  The input
** string to be tokenized is pInput[0..nBytes-1].  A cursor
** used to incrementally tokenize this string is returned in
** *ppCursor.
*/
static int simpleOpen(
  sqlite3_tokenizer *pTokenizer,         /* The tokenizer */
  const char *pInput, int nBytes,        /* String to be tokenized */
  sqlite3_tokenizer_cursor **ppCursor    /* OUT: Tokenization cursor */
){
// ...
}

/*
** Close a tokenization cursor previously opened by a call to
** simpleOpen() above.
*/
static int simpleClose(sqlite3_tokenizer_cursor *pCursor){
  simple_tokenizer_cursor *c = (simple_tokenizer_cursor *) pCursor;
  sqlite3_free(c->pToken);
  sqlite3_free(c);
  return SQLITE_OK;
}

/*
** Extract the next token from a tokenization cursor.  The cursor must
** have been opened by a prior call to simpleOpen().
*/
static int simpleNext(
  sqlite3_tokenizer_cursor *pCursor,  /* Cursor returned by simpleOpen */
  const char **ppToken,               /* OUT: *ppToken is the token text */
  int *pnBytes,                       /* OUT: Number of bytes in token */
  int *piStartOffset,                 /* OUT: Starting offset of token */
  int *piEndOffset,                   /* OUT: Ending offset of token */
  int *piPosition                     /* OUT: Position integer of token */
){
// ...
}

/*
** The set of routines that implement the simple tokenizer
*/
static const sqlite3_tokenizer_module simpleTokenizerModule = {
  0,
  simpleCreate,
  simpleDestroy,
  simpleOpen,
  simpleClose,
  simpleNext,
  0,
};

/*
** Allocate a new simple tokenizer.  Return a pointer to the new
** tokenizer in *ppModule
*/
SQLITE_PRIVATE void sqlite3Fts3SimpleTokenizerModule(
  sqlite3_tokenizer_module const**ppModule
){
  *ppModule = &simpleTokenizerModule;
}


從底部反過來往上看,至少要實作 Create, Destroy, Open, Close 和 Next,除了初始化跟資源釋放外,最重要的就是 Next 這個部分,簡單的說,就是把一串 byes 交給 Next,而 Next 每次被呼叫時,就從指定的位置往後找出一個 token 出來回傳,以此完成架構。

來做一個簡單的 changyy tokenizer 好了,只會把文章內有 changyy 開頭的 token 都取出來,其餘都 skip 掉:

/*
** Extract the next token from a tokenization cursor.  The cursor must
** have been opened by a prior call to simpleOpen().
*/
static int changyyNext(
  sqlite3_tokenizer_cursor *pCursor,  /* Cursor returned by simpleOpen */
  const char **ppToken,               /* OUT: *ppToken is the token text */
  int *pnBytes,                       /* OUT: Number of bytes in token */
  int *piStartOffset,                 /* OUT: Starting offset of token */
  int *piEndOffset,                   /* OUT: Ending offset of token */
  int *piPosition                     /* OUT: Position integer of token */
){
  changyy_tokenizer_cursor *c = (changyy_tokenizer_cursor *) pCursor;
  changyy_tokenizer *t = (changyy_tokenizer *) pCursor->pTokenizer;
  unsigned char *p = (unsigned char *)c->pInput;

  while( c->iOffset<c->nBytes ){
    int iStartOffset;

    /* Scan past delimiter characters */
    while( c->iOffset<c->nBytes && simpleDelim(t, p[c->iOffset]) ){
      c->iOffset++;
    }

    /* Count non-delimiter characters. */
    iStartOffset = c->iOffset;
    while( c->iOffset<c->nBytes && !simpleDelim(t, p[c->iOffset]) ){
      c->iOffset++;
    }

    if( c->iOffset>iStartOffset ){
      int i, n = c->iOffset-iStartOffset;
      if( n>c->nTokenAllocated ){
        char *pNew;
        c->nTokenAllocated = n+20;
        pNew = sqlite3_realloc(c->pToken, c->nTokenAllocated);
        if( !pNew ) return SQLITE_NOMEM;
        c->pToken = pNew;
      }
      for(i=0; i<n; i++){
        /* TODO(shess) This needs expansion to handle UTF-8
        ** case-insensitivity.
        */
        unsigned char ch = p[iStartOffset+i];
        c->pToken[i] = (char)((ch>='A' && ch<='Z') ? ch-'A'+'a' : ch);
      }
      *ppToken = c->pToken;
      *pnBytes = n;
      *piStartOffset = iStartOffset;
      *piEndOffset = c->iOffset;
      *piPosition = c->iToken++;
      if( !strncmp(c->pToken,"changyy", n > 7 ? 7 : n ) )
      return SQLITE_OK;
    }
  }
  return SQLITE_DONE;
}


上述程式取於 simple_tokenizer 架構,只在做後的 token 加上判斷而已 :) 如此一來,假設測資為 "changyy changes changyys world",那搜尋 changyy 跟 changyys 都可以找到,但搜尋 changes 和 world 都會找不到(因為不符合 token 規定 :P)

$ ./sqlite3
sqlite> .load /path/changyy.so
sqlite> CREATE VIRTUAL TABLE data USING fts3(tokenize=changyy);
sqlite> INSERT INTO data ('content') VALUES ('changyy changes changyys world');
sqlite> SELECT SNIPPET(data) FROM data WHERE data MATCH 'changyy';
<b>changyy</b> changes changyys world
sqlite> SELECT SNIPPET(data) FROM data WHERE data MATCH 'changes';
sqlite> SELECT SNIPPET(data) FROM data WHERE data MATCH 'changyys';
changyy changes <b>changyys</b> world
sqlite> SELECT SNIPPET(data) FROM data WHERE data MATCH 'world';
sqlite>


可以看到上述只有當 token 有被索引過的才能找出來,如此一來,就算完成偷懶的 custom tokenizer 學習吧 XD 更多操作請參考 https://github.com/changyy/sqlite-study 

2013年8月25日 星期日

[電影] 海苔億萬富翁 / The Billionaire / Top Secret / 泰國「小老板海苔」故事



大概可能有快半個月以上,在老家看了這齣電影,但只看了一半沒看完。昨晚想起來再繼續在 PPS 上看完 XD 這大概就跟電影 社群網戰(Social Network) 類似的創業型電影,但我比較喜歡小老闆海苔的故事。

小老闆海苔嗎?第一次吃是在當兵時期,同事推薦我吃看看,果真口感很不一樣呢,不過老家看電影時,也跑去買個兩包來吃,當時的心情就油了點,看了電影才會認清原來是油炸(過油)的啊。

最近周邊也有類似性格的人,我很佩服他們可以這樣不停地、不放棄地去執行所想的理想,回想起前陣子在 fb 聊天,還被學弟的老爸念了一頓:

「你們這樣跟PTT鄉民整天嘴砲有啥兩樣!」 

真是當頭棒喝啊!哈。但能夠這樣執行的人,真的是另一種幸福啊~

對小老闆海苔有興趣的人,可以多看幾篇簡介(大概是當時電影宣傳文章):

2013年8月24日 星期六

[Linux] 讓 Gitweb 支援 Markdown 顯示 @ Ubuntu 12.04

Gitweb & Markdown

之前同事很常用 Markdown 寫筆記,希望公司用的 gitweb 可以支援,所以我就稍微下海改一下 Perl 啦 XD 在改之前也有上網查了一下,其實 gitweb 本身已經有一個架構可以展現 repo/READM.html 的功能,其實搭配 git 可以弄成每次 project commit 後,自動將某個 markdown 檔案產生對應的 HTML 來用。

可在 /usr/share/gitweb/index.cgi 搜尋 READM.html 找到這片段程式碼:

# If XSS prevention is on, we don't include README.html.
# TODO: Allow a readme in some safe format.
if (!$prevent_xss && -s "$projectroot/$project/README.html") {
        print "<div class=\"title\">readme</div>\n" .
              "<div class=\"readme\">\n";
        insert_file("$projectroot/$project/README.html");
        print "\n</div>\n"; # class="readme"
}


但如此一來就侷限一個 project 只能展現的單一 markdown 檔案,思考一會後,我就選擇依照 gitweb 架構,在處理檔案時的 raw 項目,依樣畫葫做出一個 markdown 選項 XD

做法:
  1. 安裝 markdown ,讓系統支援 markdown 指定
  2. 找尋 sub git_blob_plain 程式碼,複製一份成 sub git_blob_markdown
  3. 找尋 raw 關鍵字,可以在 sub git_blob 中看到選單,只需在 raw 和 HEAD 中間插入 markdown 選單,並註冊 our %actions 處理此動作
  4. 修改 git_blob_markdown ,把它搞成讓 markdown 指令過水一下即可
片段程式碼:

our %actions = (
        ...
        "blob_plain" => \&git_blob_plain,
        "blob_markdown" => \&git_blob_markdown,
        ...
);


git_blob 片段程式碼:

$formats_nav .=
       $cgi->a({-href => href(action=>"history", -replay=>1)},
              "history") .
      " | " .
       $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
              "raw") .
      " | " .
      $cgi->a({-href => href(action=>"blob_markdown", -replay=>1)},
               "markdown") .
      " | " .

      $cgi->a({-href => href(action=>"blob",
                            hash_base=>"HEAD", file_name=>$file_name)},
              "HEAD");


git_blob_markdown:

sub git_blob_markdown {
        my $type = shift;
        my $expires;
        if (!defined $hash) {
                if (defined $file_name) {
                        my $base = $hash_base || git_get_head_hash($project);
                        $hash = git_get_hash_by_path($base, $file_name, "blob")
                                or die_error(404, "Cannot find file");
                } else {
                        die_error(400, "No file name defined");
                }
        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
                # blobs defined by non-textual hash id's can be cached
                $expires = "+1d";
        }

        open my $fd, "git ".git_cmd()." cat-file blob $hash | markdown | "
        #open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or die_error(500, "Open git-cat-file blob '$hash' failed");

        $type = "text/html";

        # "save as" filename, even when no $file_name is given
        my $save_as = "$hash";
        if (defined $file_name) {
                $save_as = $file_name;
        } elsif ($type =~ m/^text\//) {
                $save_as .= '.txt';
        }

        my $sandbox = $prevent_xss &&
                $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;


        print $cgi->header(
                -charset => 'UTF-8',
                -type => $type,
                -expires => $expires
                );
        local $/ = undef;
        binmode STDOUT, ':raw';
        print <$fd>;
        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
        close $fd;
}


比較特別的是...我還沒有真的搞懂 gitweb 或 perl 架構,像在 git_blob_markdown 裡頭,原本的架構是用 open my $fd, "-|", ... 繼續的,但我試了很久總搭不起來,就先偷懶用 "git" 代替了 :P 此外,若要增加 markdown 輸出的顯示,如 CSS 等,可以在 git_blob_markdown 下方的 printf <$fd>; 前面,去印一些 CSS 結構囉