2016年1月27日 星期三

人工智慧先驅明斯基逝世

今天新聞報導, 人工智慧研究先驅者, 麻省理工學院教授馬文明斯基 (Marvin Lee Minsky) 於 1 月 24 日因腦溢血逝於紐約, 享年 88 歲.

人工智慧先驅明斯基辭世 享壽88

明斯基是猶太人 (猶太人實在很優秀, 難怪希特勒容不下 ...),  係普林斯頓大學數學博士, 與人工智慧另一位奠基者 約翰麥卡錫 (John McCarthy, 也是普林斯頓大學數學博士) 於麻省理工學院創立人工智慧實驗室, 兩人因為在人工智慧領域的貢獻, 分別於 1969 年與 1971 年獲得有計算機科學界諾貝爾獎之稱的圖靈獎.

明斯基, 麥卡錫, 以及夏農 (Claude Shannon) 等人於 1956 年在英國達特茅斯學院召開了一個研討會, 會中麥卡錫提出以人工智慧 (AI, Artificial Intelligence) 作為此學術領域名稱, 此為 AI 一詞的由來. 達特茅斯會議的 10 位與會學者在會後的二十餘年為人工智慧的發展做出了開創性貢獻.

談到人工智慧, 不能不提到英國天才數學家, 曼徹斯特大學教授艾倫圖靈 (Allen Turing), 他所提出的圖靈測試被用來判定一個機器是否具有與人類等價之智能, 被尊稱為人工智慧之父. 他領導的小組在二戰期間為英國軍情六處破解了德軍的密碼系統 Enigma, 使德軍轉為挨打局面, 讓歐戰至少提早了兩年結束, 但其成就卻被軍情六處隱藏而不為世人所知, 反而後來因為其同性戀傾向遭到迫害, 最後為此自殺. 直到 2009 年才由首相布朗為其平反, 女王更在 2013 年公開赦免了當年他因同性戀被定的罪. 康柏拜區主演的模仿遊戲說的就是他的故事, 美國計算機協會 (ACM) 於 1966 年設立的圖靈獎, 也以其名字來命名以表彰他對電腦科學的巨大貢獻.

人工智慧研究論文最早是起始於美國神經科學家 華倫麥卡洛克 (Warren McCulloch) 與沃爾特皮次 (Walter Pitts) 於 1943 年發表的 "A Logical Calculus of the Ideas Immanent in Nervous Activity", 此論文被認為是人工智慧領域的最早期研究報告, 而明斯基就是他們的學生. 他們所提出的大腦神經元模型, 為早期的 AI 研究開闢了新的視野. 關於麥卡洛克與皮次的傳奇故事 (特別是皮次), 詳閱 :

# 陨落的天才: Walter Pitts和他违背的逻辑

關於人工智慧的歷史, 參閱 :

2016年1月25日 星期一

尾牙

今天公司在漢神海港辦尾牙, 這是首度以歐式自助餐形式舉辦, 也是我們建議了兩年後才被採納. 以往在餐廳辦, 搞得好像喜宴一樣, 又要長官致詞, 又要來賓致詞, 還要各單位輪流主辦, 上台扭腰擺尾的, 還有那些愛唱歌的上台搶麥, 唱得我們都不敢坐在台前, 不然耳朵會受不了.

今年這樣很好, 只要電腦連線台北盯緊抽獎流程即可, 沒抽到前大家埋頭苦吃, 要吃多少就有多少, 吃完也不用打包 (應該是 ... 也不能打包 ...), 非常省事. 有兩位同事因為停車時數問題, 不耐久候提前離開, 結果台北長官加碼, 竟然抽到他們, 分別是 10000 元與 5000 元獎金, 實在扼腕. 我今年槓龜, 沒半個獎. 可能去年手氣太好了, 今年讓給人家吧.


2016 第 4 周記事

這兩天終於領教甚麼是真正的冷, 回去鄉下整天下雨, 這種又濕又冷的天氣最討厭, 連在室內穿得很厚還是感到寒意逼人. 想起上回清理時在樓上找到的伊萊克斯電熱器, 趕緊拿下來看看還能不能用, 通電後果然熱烘烘, 但是看說明書上寫功耗 430 瓦, 天啊, 是吹風機的一半, 冰箱的兩倍耶! 等手掌暖和些就關掉了. 小舅說日本人常用的是煤油電暖爐, 但是我查了一下, 發現這有耗氧, 一氧化碳, 與 PM2.5 問題, 參考 :

# 比一比!總盤點8大電暖爐優缺點
# 請問煤油暖爐安全嗎?

外面冷支支, 但為了滷冬瓜, 還是得到菜園去砍甘蔗來墊鍋底. 看到土番茄雖然長成, 但是蟲害也令我灰心, 撿了掉落的一棵, 發現裡面真的有一隻大蟲 !


難道不噴農藥就沒法收成? 番茄最怕雨了, 這幾天陣雨下來不知能否耐得住摧折?

本周因姐姐要上李麥克電影觀賞課, 只有菁菁與我回鄉下. 我看等菁菁也上高中, 屆時就只有我一個人回去了. 甚麼是活在當下? 我認為不要怨天尤人就是當下.

週日中午一邊在削冬瓜皮, 一邊看公視播的印度片: 三個傻瓜, 其實已看過好幾回, 但都是攔腰看, 截尾看, 還沒從頭看到尾. 這次也是, 但只有開頭的地方沒看到. 此片讓人又笑又掉淚, 傳達的是友情, 真心, 以及人生應該照自己的興趣與才能走, 而非他人的眼光. 藍丘在結尾說的 : 我們應該追求卓越而非成就, 因為當你達到卓越了, 成就自然會追著你跑.


2016年1月21日 星期四

上台北

昨天下午坐自強號上台北, 因為今天要向大頭目們簡報. 這種苦差事沒人要做, 推來推去推到我這個太極拳火候不夠的, 我也狠不下心來拒絕, 就 ... 認了. 其實周一內部做了一次彩排, 但是我太大意了, 以為稍微準備就可以應付, 結果簡報一張一張打出來, 竟然腦中一片空白, 報告得 2266, 慘不忍睹, 被電得很慘. 這說明甚麼? 就是不要因為打了幾次勝仗, 就認為自己是不敗將軍. 每一次的挑戰都要當一回事, 做好萬全準備.

痛定思痛, 週二起我就仔細將簡報看完, 打了一份重點提示. 因為簡報不是我寫的, 所以我對內容也不是很熟, 要一一去問每一個撰寫者. 而且照理說也不是我這個層級該報告的, 但台北的大頭目說這次要讓年輕人來報告, 最好是新進人員. 卡好, 我是年輕人嗎? 我是新進人員嗎? 都不是. 既然推不掉, 就應該正面去看待才對, 但是我心裡還有點不平衡, 仗著以前幾次成功的簡報經驗, 就吊兒啷噹不當一回事, 被電老實說真的是活該.

週三是行程, 早上在家做了幾次演練, 將重點熟記在心後, 下午就匆忙趕去搭火車. 會坐火車也是因為心不在焉, 明明上司有提醒要訂高鐵早鳥票, 但就是不把它當一回事, 等週二想起去訂時已沒了, 只好訂自強號, 反正是前一天去, 時間不趕慢慢坐也無妨. 但這次坐火車讓我大為失望, 車廂左右搖晃, 讓我懷疑是不是幾次大地震把台鐵的鐵軌都搖歪了? 高鐵不會呀! 不但快還很平穩, 哪像自強號這樣一下往右扯, 一下突然往左拉, 實在讓人很不舒服. 而且那天去取票, 櫃台那個看起來就是一副標準公務員臉的鐵路局員工, 講話的口氣好像他是大官一樣. 我覺得鐵路局早該民營化才對.

以後絕對不再坐台鐵了, 寧可花多點錢買舒適, 買時間. 我竟然用寶貴的時間換省錢, 真是愚不可及, 豬一樣的思維.

這禮拜都在忙這個簡報, 今天早點睡, 明天要回頭幹正經事了.


2016年1月19日 星期二

2016 第 3 周記事

過去的一周最重要的是 14 任總統副總統選舉, 順利完成了第三次政黨輪替, 可喜可賀也. 做不好就滾下台, 這就是民主的價值, 管他甚麼黨. 我個人堅持無黨無派, 誰歪哥我就罵誰, 比較不會有偏見. 選上的小英也別太高興啦, 一堆爛攤子等著傷腦筋呢.

週六小舅來挖走了一株矮種的香蕉苗, 剩下三株, 其中一株請小舅幫我移到菜園轉角, 另外兩株因為靠太近分割不易就算了. 而且其中一株比較大棵, 正在長出嫩筍, 過幾個月應該會開花了, 硬分割可能傷到根部太深.

週六晚上在看開票結果時, 聽到救護車經過村裡往山邊去, 但沒聽到出來的笛聲, 判斷可能有人送回來. 週日爸去探聽結果, 竟然是山下的文財叔公往生矣, 享年應該八十多. 他是姑婆太的兒子, 雖然算是遠房親戚, 但婚喪喜慶仍有來往. 母親在世時跟文財叔婆也常走動, 菁菁都還記得小時候阿嬤常載她去叔公太家串門子. 上週騎單車散步時, 經過他家門口, 遠遠看到他走到路口, 本想叫他, 等我們騎近時卻又走進去了. 聽爸說周六兩夫妻還一起去投票, 下午就感到不適送醫, 說是心肌梗塞, 叔公這種走法還真是有修到哩. 他愛飲酒唱山歌, 我手上收藏的一套客家山歌就收錄了他的歌聲.


2016年1月16日 星期六

哈哈哈

哈哈哈.


騙子又出來了

騙子又出來了. 

# 選前團結之夜 馬英九懇求支持朱立倫

"馬強調,8年施政有不夠周延地方,他已痛切檢討,相信朱會記取教訓,若當選總統後會做得比他更好 (我做得已經很好了 ...);馬最後向藍營選民喊話,「懇求」大家出來投票,支持朱立倫和國民黨。"

"馬表示,過去8年所有大政方針是正確的,在執行的方式時機上也許不夠周延,引起批評,他已經痛切檢討,也做了必要改進,....." (這個 .... 還有救嗎?)

 
看九趴的怎樣罵十八趴的

我等這一天已經 2900 多天了. 仗著國會多數罷免不了這個國際認證的低能政客, 今天我一定要去投票, 行使選舉權, 同時行使罷免權. 立法院做不到的, 人民來做.

至少, 為那些優秀卻只能領 22K 的小兄弟們出口鳥氣. 

就是今天

徹底埋葬無恥政客!

2016年1月15日 星期五

就在明天

號角已經響起, 衝啊!

如何在網頁中使用網頁編輯器 TinyMCE

去年底因應 IE11 升版將工作日誌的 HTML 編輯器換新為 CKEditor 後, 最近同事反映此編輯器用起來沒有舊的 FCKeditor 好用, 我原先覺得那只是習慣問題吧! 我自己用起來還 OK 啊! 那是我花了一個禮拜才搞定改版的種種問題耶! 參考 :

# 如何在網頁中使用網頁編輯器 CKEDITOR

但使用者的體驗是真實的, 不應該漠視. 所以今天看到下列這篇, 介紹十種最好用的所見即所得 HTML 編輯器後, 發現其中的 TinyMCE 似乎不錯 : 


其展示範例見 :

https://www.tinymce.com/docs/demo/full-featured/ (全功能)

TinyMCE 是可免費使用的 Open Source 軟體, 但也提供收費升級版. 可下載 zip 檔解壓縮後放在專案目錄下 :

https://www.tinymce.com/download/

目前是 4.3.3 版, 解壓縮後會產生 tinymce/js/tinymce 的目錄結構, 只要將 js 底下的那個 tinymce 目錄放到專案目錄下即可, 我的工作日誌系統是把所有外掛放在 /plug-in 下面, 結構如下 :


裡面的 example.htm 是我自己寫的範例, 不是原 zip 檔中的內容.

TinyMCE 的用法與 CKEditor 類似, 都是利用 textarea 包裝成網頁編輯器物件 tinymce (或 tinyMCE). 只要匯入 TinyMCE 函式庫, 然後呼叫 tinymce 物件的 init() 方法, 並指定要初始化的 textarea 元件即可 :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TinyMCE 測試</title>
    <script src="tinymce.min.js"></script>
  </head>
  <body>
    <form>
      <textarea id="editor1">
      Hello World! 這是 TinyMCE!
      </textarea>
    </form>
    <script>
      tinymce.init({
        selector:'textarea'
        });
    </script>
  </body>
</html>

此處利用 selector 屬性選取 textarea 元件來初始化. 如果網頁中有多個 textarea, 那此種選取方法會將全部 textarea 元件都初始化. 也可以用 id 來選取 (CSS 用 # 表示選取 id) :

      tinymce.init({
        selector:'#editor1'
        });

這樣當兩個編輯器要做不同處理時 (例如設定不同之內容), 就要用 id 選取來個別初始化. 與 CKEditor 不同的是, 它上面多了功能選單 :

範例 1 : http://mybidrobot.allalla.com/tinymce/tinymce_1.htm


預設介面是英文版的, 如果要改為中文版, 必須另外去 TinyMCE 的Archieve 網站下載語言檔 (zip) :  

http://archive.tinymce.com/i18n/download.php?download=zh_TW

解開後會得到內含 zh_TW.js 檔的 langs 目錄, 將其複製到 tinymce 目錄下覆蓋原目錄 (裡面預設無語言檔), 然後在初始化時加入 language 屬性, 指定其值為 "zh_TW" 即可 :

      tinymce.init({
        selector:'#editor1',
        language:'zh_TW'
        });

範例 2 : http://mybidrobot.allalla.com/tinymce/tinymce_2.htm


除了自備資源外, 如果專案是放在 Internet 上, 則利用 CDN 會比較方便, 不須準備檔案, 只要將 TinyMCE 函式庫指向下列網址之一即可 :

http://cdn.tinymce.com/4/tinymce.min.js
https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.3.3/tinymce.min.js

但是 CDN 不提供語言檔, 只能用預設之英文版, 不可以使用 language 屬性去設定, 否則會因為找不到語言檔, 導致網頁無法渲染而發生錯誤. 如果使用 CDN 又要 Localization, 必須自行提供語言檔, 並用 language_url 屬性指定其位址, 如下例所示 :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TinyMCE 測試</title>
    <script src="http://cdn.tinymce.com/4/tinymce.min.js"></script>
  </head>
  <body>
    <form>
      <textarea id="editor1">
      Hello World! 這是 TinyMCE!
      </textarea>
    </form>
    <script>
      tinymce.init({
        selector:'#editor1',
        language_url:'http://mybidrobot.allalla.com/tinymce/langs/zh_TW.js'
        });
    </script>
  </body>
</html>

範例 3 : http://mybidrobot.allalla.com/tinymce/tinymce_3.htm

除了繁體中文外, 我也下載了日, 韓, 簡體中文語言檔放在 langs 下, 例如使用韓文介面就把上面的 zh_TW.js 改為 ko_KR.js 即可 (但我這免費的測試網站並不穩定, 有時會無法存取).

範例 4 : http://mybidrobot.allalla.com/tinymce/tinymce_4.htm (日文)
範例 5 : http://mybidrobot.allalla.com/tinymce/tinymce_5.htm (韓文)
範例 6 : http://mybidrobot.allalla.com/tinymce/tinymce_6.htm (簡體中文)
範例 7 : http://mybidrobot.allalla.com/tinymce/tinymce_7.htm (阿拉伯文)
範例 8 : http://mybidrobot.allalla.com/tinymce/tinymce_8.htm (俄文)






預設的工具列與功能表乃最簡設定, TinyMCE 提供了 plugins 與 toobar 屬性來自定工具列, 其值為一個由工具按鈕名稱組成的陣列. 工具列上的按鈕分屬於不同的 plugins, 參考 :

# Buttons/controls

因此如果要擺上某個按鈕, 那麼其所屬的 plugins 也要列在 plugins 陣列內, 否則會沒效果. 花了一晚上測試工具列的組合, 終於得到比較滿意的排列, 如範例 9 所示 :

      tinymce.init({
        selector:'#editor1',
        language_url:'http://mybidrobot.allalla.com/tinymce/langs/zh_TW.js',
        height:300,
        width:800,
        plugins:[
          'advlist autolink lists link image charmap print preview anchor',
          'searchreplace visualblocks code fullscreen textcolor colorpicker',
          'insertdatetime media table contextmenu paste code hr pagebreak nonbreaking'
          ],
        toolbar:['newdocument preview fullscreen code print searchreplace selectall | bold italic underline strikethrough superscript subscript removeformat forecolor backcolor | alignleft aligncenter alignright alignjustify |',
        'undo redo cut copy paste pastetext pasteword | bullist numlist outdent indent | blockquote nonbreaking hr pagebreak charmap anchor link unlink image table']
        });

範例 9 : http://mybidrobot.allalla.com/tinymce/tinymce_9.htm


這裡我們也使用 width 與 height 設定編輯器的大小, 當然也可以在 textarea 加上 css 樣式來設定. 在 toolbar 屬性值陣列中, 管線符號用來分群, 逗號則用來跳行. 注意, 管線符號與工具按鈕名稱之間必須用空格隔開, 如果黏在一起會出現錯誤. 其次, 搜尋鈕右邊的全選 (selectall) 按鈕圖像不知何故沒有顯現, 但其實是有作用的.

如果不需要功能選項, 可以將 menubar 屬性設為 false 即可, 這樣如下範例 10 所示 :

      tinymce.init({
        selector:'#editor1',
        language_url:'http://mybidrobot.allalla.com/tinymce/langs/zh_TW.js',
        height:300,
        width:800,
        menubar:false,
        plugins:[
          'advlist autolink lists link image charmap print preview anchor',
          'searchreplace visualblocks code fullscreen textcolor colorpicker',
          'insertdatetime media table contextmenu paste code hr pagebreak nonbreaking'
          ],
        toolbar:['newdocument preview fullscreen code print searchreplace selectall | bold italic underline strikethrough superscript subscript removeformat forecolor backcolor | alignleft aligncenter alignright alignjustify |',
        'undo redo cut copy paste pastetext pasteword | bullist numlist outdent indent | blockquote nonbreaking hr pagebreak charmap anchor link unlink image table']
        });

範例 10 : http://mybidrobot.allalla.com/tinymce/tinymce_10.htm


接下來是重頭戲, 要測試 TinyMCE 的設值 setContent() 與取值方法 getContent(), 這是網頁專案中一定會用到的, 例如編輯工作日誌中的紀錄時, 要從資料庫中取出欄位值, 將其設值給 TinyMCE 編輯器; 而要存回資料庫時, 須從 TinyMCE 編輯器中取值, 再回傳給後端處理. 參考 :

http://archive.tinymce.com/wiki.php/API3:method.tinymce.Editor.getContent
http://archive.tinymce.com/wiki.php/API3:method.tinymce.Editor.setContent

不過 setContent() 與 getContent() 這兩個方法不能直接在 init() 後使用, 否則會出現 "Uncaught TypeError: Cannot read property 'getContent' of null" 錯誤, 如下面範例 11 所示 :

範例 11 : http://mybidrobot.allalla.com/tinymce/tinymce_11.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TinyMCE 測試</title>
    <script src="http://cdn.tinymce.com/4/tinymce.min.js"></script>
  </head>
  <body>
    <form>
      <textarea id="editor1">TinyMCE</textarea>
    </form>
    <script type="text/javascript">
      tinyMCE.init({
        selector:'#editor1'
        });
      alert(tinyMCE.get("editor1").getContent());
      tinyMCE.get("editor1").setContent('Hello World! 這是 TinyMCE!');
    </script>
  </body>
</html>

必須放在方法中, 然後用個按鈕來呼叫它才可以, 如下範例 12 :

範例 12 : http://mybidrobot.allalla.com/tinymce/tinymce_12.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TinyMCE 測試</title>
    <script src="http://cdn.tinymce.com/4/tinymce.min.js"></script>
  </head>
  <body>
    <form>
      <textarea id="editor1">TinyMCE</textarea>
      <button onclick="get_content()">Get content</button>
      <button onclick="set_content()">Set content</button>
    </form>
    <script>
      tinyMCE.init({
        selector:'#editor1'
        });
      function get_content(){
        var content=tinymce.get("editor1").getContent();  //以 id 取得物件
        alert(content);
        }
      function set_content(){
        tinymce.get("editor1").setContent('Hello World! 這是 TinyMCE!');
        }
    </script>
  </body>
</html>

但是這個範例有一個問題, 就是設值後會重新 init 編輯器, 導致所設的值像曇花一現一下子又變回預設值了. 原因是我們有用 form 元素之故, 當按了按鈕後會向後端提交, 但沒有指定 action, 這樣會向本頁提交, 結果就重新載入本網頁, 編輯器內容當然就重設為預設值了 :

範例 13 : http://mybidrobot.allalla.com/tinymce/tinymce_13.htm

這樣就能單純地驗證 getContent() 與 setContent() 方法了. 當然, 在與後端正常的互動中一定要有 form 表單元素才行, 我們可以在按下確定鈕後呼叫一個方法, 再用 getContent() 取得編輯器內容, 然後再呼叫 submit() 提交表單, 如下範例 14 所示 :

範例 14 : http://mybidrobot.allalla.com/tinymce/tinymce_14.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TinyMCE 測試</title>
    <script src="http://cdn.tinymce.com/4/tinymce.min.js"></script>
  </head>
  <body>
    <form method="post" action="get_content.php">
      <textarea id="editor1" name="editor1">TinyMCE</textarea>
      <button onclick="check(this.form)">Submit</button>
      <input type="hidden" name="content">
    </form>
    <script>
      tinyMCE.init({      
        selector:'#editor1',
        auto_focus:'editor1'
        });
      function check(formObj){
        var content=tinymce.get("editor1").getContent();
        content="編輯的內容 : <br>" + content;
        formObj.content.value=content;
        alert(content);
        formObj.submit();
        }
    </script>
  </body>
</html>

在這個範例中, 我在表單中增加一個名為 content 的隱藏元件, 當按下 Submit 按鈕時會先呼叫 check() 方法, 並傳入表單物件 this.form, 然後在 check() 中用 getContent() 取得編輯器內容, 在其前冠上額外資訊後, 設值給隱藏元件 content 後向後端 get_content.php 以 post 方法提交表單, 此後端程式很簡單, 就單純取得所傳遞的 content 參數後輸出給前端而已 :

<?php
header('Content-Type: text/html;charset=UTF-8');
echo $_POST['content'];
?>



注意, 這裡我們在初始化編輯器時, 加入了一個新屬性 auto_focus, 其值為編輯器的 id 屬性值 (但不可加 #), 這可以讓網頁載入時讓編輯器自動取得焦點 (也就是游標會在編輯區內一閃一閃的), 參考 :

https://www.tinymce.com/docs/configure/integration-and-setup/#auto_focus
http://fiddle.tinymce.com/
http://stackoverflow.com/questions/31475325/tinymce-get-content
http://fiddle.tinymce.com/WSeaab/1

其次, 編輯器物件不管是用 tinymce 或 tinyMCE 都可以, 都是別名.

以上測試範例檔可從下面網址下載 :

下載測試範例

其他參考資料 :

How do I set content in TinyMCE 4, preferably within the $(document).ready(); block?
# how to enable font family and color options in tinymce editor?
https://www.tinymce.com/docs/plugins/colorpicker/
# TinyMCE width and height disobedient!


2016年1月13日 星期三

2016 第 2 周記事

又到星期三了, 過去這禮拜三隻小狐狸都要準備段考不能回鄉下, 所以只有 わたし 一個人回去, 感覺有點孤寂哩. 上週採收的香蕉由於最近天氣比較冷, 我帶了兩串到高雄過了一個禮拜都還是青青的. 留在鄉下的幾串卻黃得稍快, 已有一串變黃色. 週日中午小舅來時給了他們兩串帶回去.

香蕉採收後功成身退, 爸就把香蕉樹砍掉了做肥料, 軀幹躺在地上似乎有點可憐. 但不這樣做的話, 旁邊長出的四株幼苗會長不好. 最近要想辦法將側芽切割後分種, 因這矮種的採收較方便 :


而玉米目前有三批還在成長, 最快的十株已齊腰了 :


第二批有 20 株的則至膝蓋高 :


最近的一批也是 20 株, 其中一半是白玉米, 沒有隔很遠種植, 授粉時可能會混雜為黃白玉米 :


而南瓜葉也開始乾枯, 可以採收了, 這次廚餘自然長出的南瓜有兩個品種, 一種表面是亮光像打過蠟; 另一種則是表面有一層白粉似的 :



採摘後先放個兩周再說. 爸說市集那邊一顆才賣十多塊, 自己種好像沒啥價值, 還要施點肥料. 但不種的話也是長草, 反正就廚餘埋在那邊就會自然發芽了. 

上週五 (1/8) 中午休息時間跑去林森路中華電信申請節能補助, 前面排了十個人左右, 等了大概半小時就領到 2000 元補助了, 想到剛開始時新聞報說一堆人排了一整天卻因為每日限 50 個人而幹噍, 真是好笑!


2016年1月12日 星期二

換浴室通風扇

今天要下午才進辦公室, 早上寫 GAE 程式告一段落後, 先做室內大掃除, 清理後門紗窗以及廚房櫃子, 把裡面已塞了多年的雜物通通丟掉, 抽屜洗一洗拿去曬. 然後拿菁菁的外套去繡學號, 順便到特力屋去買浴室通風扇.

前幾天主臥室浴室的通風扇發出咖咖聲, 我知道這馬達又出問題了, 才兩三天就嘎然而止, 完全靜止不動了. 我看之前寫在外殼上的日期是 2014-04-13, 才一年九個月就 OVER, 真是太爛了. 這款是太星電工的產品 (喜馬拉雅系列) :


我已經買這家兩次了, 上一回也是太星電工, 但款式不一樣, 用了兩年多一點就掛了. 而外面那間浴室的從交屋用到現在已經快 20 年了 (順光牌), 雖然已經有噪音了, 但至少都還能用啊! 主臥室以前那個同品牌的也是用到 16 年才壞掉. 現在的工業產品都不像以前的耐用, 可能是設計成一過保固就兩光, 這樣廠商才會有生意. 難怪耐用的順光牌現在已經找不到了.

這次改買特力屋委託台中昶星製造的自有品牌 Very 系列型號 BQ-358, 629 元中價位, 保固 2 年, 功耗 20W. 左邊那台它牌 "阿拉斯加" 系列的看來也不錯, 但因為沒有寫保固幾年, 所以不考慮. 本來考慮買好一點的台達電, 變頻馬達功耗僅 7.5W, 很省電, 但 1980 元的價位實在有點貴, 省下的電恐怕要運轉好幾年才回本, 雖然保固 3 年, 還是不划算, 這價格可買特力屋的三台耶!


下午打電話去特力屋問, 太星電工保固多久, 卡好, 就是一年啦, 真準. 之前還買過兩個移動偵測照明 LED, 其中一個就是太星電工的, 才幾個月就完全不會亮了, 而它牌的到現在還很好用. 以後這家的產品, 列為拒絕往來戶囉, 沙優哪啦!

2016-01-14 補充 :

找到了,老牌的順光通風扇找到了, PC Home 有在賣 :

# 順光衛浴通風嵌入式換氣機SWF-15 $749


2016年1月8日 星期五

如何在 GAE 上佈署 jQuery EasyUI 專案 (五) : 紀錄到訪者

測試完 Google 帳號驗證後, 我回頭繼續 GAE 資料儲存區 (Datastore) 的測試. 之前使用 PHP 開發 EasyUICMS 時, 裡面有一個功能是紀錄來過網站首頁的使用者, 將訪客的 IP 位址, 拜訪時間, 以及使用之客戶端軟體 (瀏覽器) 存入資料表中, 藉以了解訪客來自何方. 這在 GAE 上要如何實作此功能呢?

以下測試是在上一篇文章的基礎上加以修改而得, 參考 :

# 如何在 GAE 上佈署 jQuery EasyUI 專案 (四) : Google 登入

首先要在資料庫定義檔 model.py 中新增一個 Visitors 資料表來記錄瀏覽網站首頁 (根目錄) 的訪客資訊, 包含 ip (位址), visit_time(到訪時間), 以及 user_agent (瀏覽器) 三個欄位 :

# -*- coding: utf-8 -*-
from google.appengine.ext import db

class Members(db.Model):
    account=db.StringProperty()
    password=db.StringProperty()

class Visitors(db.Model):
    ip=db.StringProperty()
    visit_time=db.DateTimeProperty()
    user_agent=db.StringProperty()

member=Members(account="admin",password="aaa")
member.put()
member=Members(account="guest",password="guest")
member.put()

注意, 這裡一般來說我們會在到訪時間欄位傳入 auto_now_add 這個參數 :

visit_time=DateTimeProperty(auto_now_add=True)

這樣當新增到訪紀錄時, 此欄不須給予值就會自動填入時間. 但是因為 GAE 預設是 UTC 時區, 因此自動填入的是 UTC 的時間, 而不是台北時間. 解決此問題的辦法就是不要自動填入, 而是經過調整後自行填入.

要在訪客瀏覽首頁時紀錄其資料, 必須修改主程式 main.py 裡應用程式首頁之路徑處理類別 MainHandler, 加入記錄訪客資訊之程式碼, 修改為如下 :

class MainHandler(webapp2.RequestHandler):
    def get(self):
        ip=self.request.remote_addr
        user_agent=os.environ.get("HTTP_USER_AGENT")
        #user_agent=self.request.headers.get("User-Agent")
        visitor=m.Visitors()
        visitor.ip=ip
        visitor.visit_time=datetime.now() + timedelta(hours=+8)
        visitor.user_agent=user_agent
        visitor.put()
        url="templates/default.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

上面藍色部分是新增的程式碼, 當訪客瀏覽首頁時, 便將其 IP, 到訪時間與使用之瀏覽器, 存入資料儲存區, 然後再渲染首頁. 此處 User agent 可以用 os 模組的 environ 物件的 get() 方法取得 (需匯入 os 模組), 也可以用 request.headers 的 get() 方法取得. 而到訪時間則使用 datetime 物件的 now() 方 法取得, 再用 timedelta 物件將 UTC 時間往前挪 8 小時, 因此需先匯入 os, datetime, 以及 timedelta 這 3 個模組 :

import model as m
import os
from datetime import timedelta, datetime

關於時區調整問題, 參考 :

# GAE 的時區問題

然後在 main.py 中添加一個 list_visitors_1 的路徑以及其處理類別如下 :

class list_visitors_1(webapp2.RequestHandler):
    def get(self):
        query=m.Visitors.all()
        query.order("-visit_time")
        url="templates/list_visitors_1.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"visitors":query})
        self.response.out.write(content)

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4),
    ('/google_user_login_5', google_user_login_5),
    ('/list_visitors_1', list_visitors_1)
], debug=True)

此處 list_visitors_1 類別中的 m 為資料模型物件 model 的別名, 我們在上一篇中有將 model.py 匯入 main.py, 並為了簡單起見用 as 將其改命名為 m 物件 :

import model as m

如果沒有用 as 改名, 就要用原名 model. 這裡用 m.Visitors.all() 可以取得 Visitors 類別的所有 Querry 物件實體, 也就是瀏覽首頁者在 MainHandler 類別中所建立之拜訪資料. 然後呼叫 order() 方法將這些實體排序, 傳入參數 "-visit_time" 表示根據 visit_time 進行反向排序 (沒有負號表示正向排序, 有負號為反向排序), 這樣就會把最近的訪客排在最前面. 然後在渲染 list_visitors_1.htm 這個模板檔案時將這些 querry 實體集合當作 template 參數傳入 render() 方法中.

最後就是在 templates 目錄下繼承 jqueryeasyui.htm 這個模板寫一個 list_visitors_1.htm 來倒序顯示訪客列表如下 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table class="easyui-datagrid" title="訪客列表" style="width:960px;height:400px" data-options="singleSelect:true,collapsible:true,rownumbers:true">
    <thead>
      <tr>
        <th data-options="field:'ip',width:100">IP 位址</th>
        <th data-options="field:'visit_time',width:150">到訪時間</th>
        <th data-options="field:'user_agent'">使用瀏覽器</th>
      </tr>
    </thead>
    <tbody>
{% for visitor in visitors %}
      <tr>
        <td>{{visitor.ip}}</td>
        <td>{{visitor.visit_time|date:"Y-m-d H:i:s"}}</td>
        <td>{{visitor.user_agent}}</td>
      </tr>
{% endfor %}
    </tbody>
  </table>
{% endblock%}

這裡使用 EasyUI 的 Datagrid 來顯示訪客列表, 並以 template 引擎之 for 迴圈語法遍歷傳入之 visitors 參數以產生表格內容之三個欄位. 注意第二欄位 (到訪時間) 我用了管線過濾器 "|" 來調整日期的顯示樣式為較簡潔的 "2016-01-06 10:20:11" 格式, 原始格式為 "Jan. 6, 2016, 10:20:11 am".

測試 1 : http://jqueryeasyui.appspot.com/list_visitors_1 


關於 EasyUI 的 Datagrid 用法, 參考 :

# jQuery EasyUI 測試 : Datagrid (一)

上面測試 1 就採用了其中的範例 4 做法, 直接將表格內容輸出於頁面上. jQuery 最迷人的地方之一是提供了簡單好用的 Ajax 功能, 下面測試 2 就是改用 Ajax 方式來實作同樣功能, 以 HTTPXML 方式向後端取得 Datagrid 所需的資料, 參考 jQuery EasyUI 測試 : Datagrid (一) 中的範例 2.

首先在 main.py 中添加 list_visitors_2 與 get_visitors_2 這兩個路徑, 分別用來渲染 HTML 檔與取得後端資料 :

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4),
    ('/google_user_login_5', google_user_login_5),
    ('/list_visitors_1', list_visitors_1),
    ('/list_visitors_2', list_visitors_2),
    ('/get_visitors_2', get_visitors_2)
], debug=True)

然後撰寫 list_visitors_2 的路徑處理類別 :

class list_visitors_2(webapp2.RequestHandler):
    def get(self):
        url="templates/list_visitors_2.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

很簡單, 就是渲染 templates 下的模板檔案 list_visitors_2.htm 而已, 此檔內容如下 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="visitors" title="訪客列表" style="width:960px;height:400px"></table>
  <script language="javascript">
    $(document).ready(function(){
      $('#visitors').datagrid({
        url:'/get_visitors_2',
        method:'get',
        columns:[[
          {field:'ip',title:'IP 位址',width:150},
          {field:'visit_time',title:'到訪時間',width:150},
          {field:'user_agent',title:'瀏覽器'}
          ]],
        singleSelect:true,
        collapsible:true,
        rownumbers:true
        });
      });
  </script>
{% endblock%}

這裡很簡單, 就是繼承 jqueryeasyui.htm 模板, 放一個有 id 的 table 元件, 然後在 Javascript 程式中呼叫此 table 元件之包裹物件的 datagrid() 方法來起始 EasyUI Datagrid 物件. 其中與 Ajax 相關的兩個最重要參數是 url 與 method, 前者表示要向後端 get_visitors_2 這個路徑要求提供 JSON 資料; 後者為要求的方法.

最後只要搞定供檔程式 get_visitos_2 這個類別即可, 由於此類別要傳回 json 檔給前端, 需要用到 json 模組, 因此要先匯入 :

import json

Ajax 供檔程式如下, 注意配合上面 list_visitors_2.htm 中指定 Ajax 使用 get 方法提出要求, 這裡必須定義 get 函式才行 (反之, 若為 post 則下面就要定義 post) :

class get_visitors_2(webapp2.RequestHandler):
    def get(self):
        self.response.headers["Content-Type"]="application/json"
        query=m.Visitors.all()
        query.order("-visit_time")
        rows=[]
        count=0
        for v in query:
            visit_time=v.visit_time.strftime("%Y-%m-%d %H:%M:%S")
            visitor={"ip":v.ip,
                     "visit_time":visit_time,
                     "user_agent":v.user_agent}
            rows.append(visitor)
            count=count + 1
        obj={"total":count,"rows":rows}
        self.response.out.write(json.dumps(obj))

首先是設定 response 物件的 headers[] 串列設定 http 回應訊息的內容為 json 型態, 然後呼叫 model 的 Visitors 類別的 all() 方法查詢全部 Visitors 實體 (及訪客資料物件), 依到訪時間倒序排列. 再設定一個串列 rows 來儲存訪客物件實體, 以及一個計數器 count 來累計實體總數, 因為 EasyUI 的 Datagrid 需要此數據. 接下來用一個 for 迴圈來遍歷從資料儲存中查詢到的 Query 物件, 將其內容製作成 dictionary 物件, 一一放進串列中. 最後組合成 Datagrid 所需要格式的物件, 利用 json 類別的 dumps() 方法進行編碼後輸出即可. 參考 :

How to properly output JSON with app engine Python webapp2?
Convert a python dict to a string and back
Json概述以及python對json的相關操作
Python处理JSON

另外, 到訪時間 visit_time 原始格式無法順利通過編碼 (無法序列化, serialization), 必須經過 strftime() 函式轉為例如 "2016-01-07 01:02:03" 的字串格式才行. 參考 :

# Python datetime to string without microsecond component

此供檔程式產出的資料節錄如下 :

{"rows": [{"ip": "66.249.79.16", "visit_time": "2016-01-07 15:18:23", "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12F70 Safari/600.1.4 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"}, ... ,
{"ip": "59.127.170.38", "visit_time": "2016-01-06 22:40:27", "user_agent": "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"}],  "total": 28}

完整 json 檔可由下面超連結下載 :

http://jqueryeasyui.appspot.com/get_visitors_2

Ajax 產生的訪客列表如下面測試 2 所示 :

測試 2 : http://jqueryeasyui.appspot.com/list_visitors_2 

上面兩個測試 "理論上" 是顯示全部訪客紀錄, 如果訪客不斷增加, 列表長度就會增長, 解決之道是以分頁 (paging) 方式處理. 參考 jQuery EasyUI 測試 : Datagrid (一) 中的範例 7, 改寫為 GAE 版的分頁表格.

先在 templates 目錄下複製上面的 list_visitors_2.htm 為 list_visitors_3.htm, 然後加入 pagination:true 與 pageSize:10 這兩個分頁相關屬性如下 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="visitors" title="訪客列表" style="width:960px;"></table>
  <script language="javascript">
    $(document).ready(function(){
      $('#visitors').datagrid({
        url:'/get_visitors_3',
        method:"post",
        columns:[[
          {field:'ip',title:'IP 位址',width:150},
          {field:'visit_time',title:'到訪時間',width:150},
          {field:'user_agent',title:'瀏覽器'}
          ]],
        singleSelect:true,
        collapsible:true,
        rownumbers:true,
        pagination:true,
        pageSize:10
        });
      });
  </script>
{% endblock%}

這個 pageSize 就是一個分頁顯示的紀錄筆數, 這個值預設是 10 筆, EasyUI 的 Datagrid 有下拉式選單可以讓使用者更改每頁筆數. 當變換顯示頁時, Datagrid 元件會向 url 所指的後端路徑程式 /get_visitors_3 傳送兩個參數 page 與 rows, 分別表示欲查詢的頁次與每頁筆數. 注意, 這裡我拿掉了 table 元件的 height 屬性, 而是由每頁筆數來決定 Datagrid 的高度. 另外,  提交 Ajax 要求的方式也改成 post, 因此

接著修改主控程式 main.py, 加入 list_visitors_3 與 get_visitors_3 這兩個路徑與其處理類別, 分別用來渲染 list_visitors_3.htm 網頁與透過 Ajax 取得分頁紀錄 :

class list_visitors_3(webapp2.RequestHandler):
    def get(self):
        url="templates/list_visitors_3.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_visitors_3(webapp2.RequestHandler):
    def post(self):
        page=self.request.get("page")
        rows=self.request.get("rows")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        query=m.Visitors.all()
        query.order("-visit_time")
        visitors=query.fetch(rows, (page-1)*rows)
        rows=[]
        for v in visitors:
            visit_time=v.visit_time.strftime("%Y-%m-%d %H:%M:%S")
            visitor={"ip":v.ip,
                     "visit_time":visit_time,
                     "user_agent":v.user_agent}
            rows.append(visitor)
        count=query.count()
        obj={"total":count,"rows":rows}
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4),
    ('/google_user_login_5', google_user_login_5),
    ('/list_visitors_1', list_visitors_1),
    ('/list_visitors_2', list_visitors_2),
    ('/get_visitors_2', get_visitors_2),
    ('/list_visitors_3', list_visitors_3),
    ('/get_visitors_3', get_visitors_3)
], debug=True)

上面主要的變化是在負責 Ajax 供檔的 get_visitors_3 類別, 由於前端改用了 post 方法提交 Ajax, 因此處理類別也需定義 post 函式才行.

首先用 request 物件的 get() 方法取得前端 Datagrid 元件傳來的 page (頁次) 與 rows (每頁筆數) 參數, 然後用 len() 長度函數偵測否有傳出這兩個參數, 若有傳出參數就用 int() 函式轉成整數, 否則就設定預設值為第 1 頁與 10 筆. 然後查詢全部 Visitors 物件再以到訪日期倒序排序, 再用 fetch() 方法擷取指定頁之 Visitors 實體, 其第一參數是要擷取之筆數, 故傳入 rows; 第二參數是資料實體的索引, 即頁次減 1 後乘以每頁筆數即得. 例如每頁 10 筆, 則第 2 頁是從索引 (2-1)*10=10 開始 (第 1 頁是 0~9). 接下來就跟上面測試 2 一樣, 將每筆到訪記錄組成 dict 後存入串列.

最重要的是, 由於不是遍歷所有 Visitors 實體, 無法透過迴圈計算顯示的總筆數, 改由呼叫 Query 物件的 count() 方法來取得總筆數. 據書上寫說此方法效能不好, 但至少能用, 實際範例如下 :

測試 3 : http://jqueryeasyui.appspot.com/list_visitors_3 


可見取消 table 的 height 樣式後, 右方捲軸就消失了.

EasyUI 的 Datagrid 欄位標題可以透過將欄位設定為 sortable:true 而具有排序功能, 當點擊欄位標題時, 也會啟動 Ajax 查詢, 並向後端增加傳送 sort (要排序之欄位名稱) 以及 order (順序/倒序) 這兩個參數, 參考 jQuery EasyUI 測試 : Datagrid (一) 中的範例 9. 下面測試 4 就來實作欄位排序功能.

首先複製測試 3 的模板 list_visitors_3.htm, 修改三個欄位設定, 加入 sortable:true 屬性, 存成 list_visitos_4.htm 如下 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="visitors" title="訪客列表" style="width:960px;"></table>
  <script language="javascript">
    $(document).ready(function(){
      $('#visitors').datagrid({
        url:'/get_visitors_4',
        method:"post",
        columns:[[
          {field:'ip',title:'IP 位址',width:150,sortable:true},
          {field:'visit_time',title:'到訪時間',width:150,sortable:true},
          {field:'user_agent',title:'瀏覽器',sortable:true}
          ]],
        singleSelect:true,
        collapsible:true,
        rownumbers:true,
        pagination:true,
        pageSize:10
        });
      });
  </script>
{% endblock%}

然後在 main.py 中增加 list_visitors_4 與 get_visitors_4 的路徑與處理類別 :

class list_visitors_4(webapp2.RequestHandler):
    def get(self):
        url="templates/list_visitors_4.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_visitors_4(webapp2.RequestHandler):
    def post(self):
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="visit_time"
        if not len(order):
            order="desc"
        query=m.Visitors.gql("ORDER BY %s %s" % (sort, order))
        visitors=query.fetch(rows, (page-1)*rows)
        rows=[]
        for v in visitors:
            visit_time=v.visit_time.strftime("%Y-%m-%d %H:%M:%S")
            visitor={"ip":v.ip,
                     "visit_time":visit_time,
                     "user_agent":v.user_agent}
            rows.append(visitor)
        count=query.count()
        obj={"total":count,"rows":rows}
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4),
    ('/google_user_login_5', google_user_login_5),
    ('/list_visitors_1', list_visitors_1),
    ('/list_visitors_2', list_visitors_2),
    ('/get_visitors_2', get_visitors_2),
    ('/list_visitors_3', list_visitors_3),
    ('/get_visitors_3', get_visitors_3),
    ('/list_visitors_4', list_visitors_4),
    ('/get_visitors_4', get_visitors_4)
], debug=True)

這裡改變較多的是在 get_visitors_4 路徑處理類別, 取得 sort 與 order 參數後利用 len() 函式偵測有無傳出值, 若無就分別預設為 "visit_time"  與 "DESC". 其次我們得改用資料類別的 gql() 方法或 db 模組的 GqlQuerry() 方法來查詢資料儲存區, 因為欄位排序必須用到 GQL 語言才能達成目的. 這裡我們用 % 格式化字串來將 sort 與 order 參數填入 "ORDER BY ..." 語句中, 這樣 "理論上" 就得到經排序後的全部訪客資料實體了, 再使用 Query 物件的 fetch() 方法即可得到指定頁次的資料了, 其餘與上面測試 3 一樣. 實際範例如下 :

測試 4 : http://jqueryeasyui.appspot.com/list_visitors_4 


可見欄位標題右邊多了三角形圖案, 表示為可排序欄位, 點欄位標題即可針對該欄位進行順向或逆向排序. 其實上面的 gql() 查詢也可以用 order() 函式來做, 將 gql() 那行用下列取代 :

        query=m.Visitors.all()
        if order=="desc":
            query.order("-" + sort)
        else:
            query.order(sort)

效果是一樣, 這裡是判斷排序方向, 若為倒序就在排序欄位前面加個負號.

接下來要實作搜尋功能, 參考 jQuery EasyUI 測試 : Datagrid (一) 中的範例 10, 只要在 table 中增加 toolbar 屬性, 就可以在 Datagrid 上方添加搜尋工具列, 如下面 list_visitors_5.htm 所示 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="visitors" title="訪客列表" style="width:960px;" toolbar="#search_bar"></table>
  <div id="search_bar" style="padding:3px">
    <select id="search_field">
      <option value="ip">IP 位址</option>
      <option value="visit_time">到訪時間</option>
      <option value="user_agent">瀏覽器</option>
    </select>
    <input id="search_what" style="line-height:15px;border:1px solid #ccc">
    <a href="#" class="easyui-linkbutton" iconCls="icon-search" onclick="doSearch()">搜尋</a>
  </div>
  <script language="javascript">
    $(document).ready(function(){
      $('#visitors').datagrid({
        url:'/get_visitors_5',
        method:"post",
        columns:[[
          {field:'ip',title:'IP 位址',width:150,sortable:true},
          {field:'visit_time',title:'到訪時間',width:150,sortable:true},
          {field:'user_agent',title:'瀏覽器',sortable:true}
          ]],
        singleSelect:true,
        collapsible:true,
        rownumbers:true,
        pagination:true,
        pageSize:10
        });
      });
    function doSearch(){
      $('#visitors').datagrid('load',{
        search_field: $('#search_field').val(),
        search_what: $('#search_what').val()
        });
      }
  </script>
{% endblock%}

此處使用 div 製作了一個包含下拉式選單, 文字欄位, 以及按鈕組成的搜尋框, 以 Datagrid 的 toolbar 屬性掛到其工具列去, 當按下搜尋鈕時, 執行 doSearch() 函式, 取得 search_field 與 search_what 之值, 呼叫 Datagrid 的 load() 方法以 Ajax 方式重新載入表格內容, 這會將 search_field 與 search_what 當作參數傳給後端去做搜尋. 但很可惜的, GAE 的 GQL 不提供 SQL 中的 LIKE 語法, 這使得 LIKE 'text%' 或 LIKE '%text' 或 LIKE '%ext%' 等模糊比對功能無法達成, 不過可以透過字串排序方式做到 LIKE 'text%' , 參考 :

在GAE中模拟like查询进行模糊搜索
Google App Engine: Is it possible to do a Gql LIKE query?

修改 main.py, 增添 list_visitors_5 與 get_visitors_5 路徑與處理類別如下 :

class list_visitors_5(webapp2.RequestHandler):
    def get(self):
        url="templates/list_visitors_5.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_visitors_5(webapp2.RequestHandler):
    def post(self):
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        search_field=self.request.get("search_field")
        search_what=self.request.get("search_what")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="visit_time"
        if not len(order):
            order="desc"
        if len(search_field):
            query=m.Visitors.all()
            query.filter(search_field + " >= ", search_what)
            query.filter(search_field + " < ", search_what + u'\ufffd'
        else:
            query=m.Visitors.gql("ORDER BY %s %s" % (sort, order))
        visitors=query.fetch(rows, (page-1)*rows)
        rows=[]
        for v in visitors:
            visit_time=v.visit_time.strftime("%Y-%m-%d %H:%M:%S")
            visitor={"ip":v.ip,
                     "visit_time":visit_time,
                     "user_agent":v.user_agent}
            rows.append(visitor)
        count=query.count()
        obj={"total":count,"rows":rows}
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4),
    ('/google_user_login_5', google_user_login_5),
    ('/list_visitors_1', list_visitors_1),
    ('/list_visitors_2', list_visitors_2),
    ('/get_visitors_2', get_visitors_2),
    ('/list_visitors_3', list_visitors_3),
    ('/get_visitors_3', get_visitors_3),
    ('/list_visitors_4', list_visitors_4),
    ('/get_visitors_4', get_visitors_4),
    ('/list_visitors_5', list_visitors_5),
    ('/get_visitors_5', get_visitors_5)
], debug=True)

這裡我們新增擷取搜尋框傳出的 search_field 與 search_what, 然後判斷前端是否有傳出 search_field 參數, 沒有的話就如同上面測試 4 依指定方式排序; 若有, 就先取得該資料表全部紀錄, 再用 filter() 函數去找搜尋欄位之值介於 search_what 字串與 search_what + u'\ufffd' 字串間的紀錄, 這裡 u'\ufffd" 是最後一個 unicode 字元, 因此就能搜尋到以 search_what 開頭的紀錄.

測試 5 : http://jqueryeasyui.appspot.com/list_visitors_5 

其實上面用 >= 與 < 配合 unicode 模擬 LIKE "search_what%" 後面模糊搜尋之前採用 gql() 查詢都失敗, 例如用下列語法 :

            query=m.Visitors.gql(
                "WHERE %s >= %s AND %s < %s ORDER BY %s %s" %
                (search_field, search_what, search_field,
                search_what + u'\ufffd', sort, order))

執行時出現 "Internal Server Error", 錯誤訊息如下 :

"BadQueryError: Parse Error: Expected no additional symbols at symbol \ufffd"

怎麼看都看不出問題在哪. 就算不跳行, 把整個 GQL 語句放在一行也是一樣的錯誤. 換另外一種字串代換法也無濟於事 :

            query=m.Visitors.gql(
                'WHERE :1 >= :2 AND :3 < :4 ORDER BY :5 :6',
                search_field, search_what, search_field,
                search_what + u'\ufffd', sort, order)

此語句出現的錯誤是 :

"BadQueryError: Parse Error: Invalid WHERE Identifier at symbol :1"

最後只剩下 filter() 函式這一招, 竟然就成功了. 總之,  可能是我的 Python 還不到家, 要好好地來學學, 畢竟 Raspberry Pi 也要用. 不過上面這個測試 5 還有一個問題, 就是欄位排序功能不能用了, 我試過先用 gql() 排序, 再用 filter() 過濾 :

            query=m.Visitors.gql("ORDER BY %s %s" % (sort, order))
            query.filter(search_field + " >= ", search_what)
            query.filter(search_field + " < ", search_what + u'\ufffd')

但這樣是不行的, 出現如下伺服器錯誤 :

"'GqlQuery' object has no attribute 'filter'"

原來 gql() 傳回的 Query 物件與用 all() 傳回者不同, 它沒有 filter() 函式. 但如果改成用 all() 搜尋後再呼叫 sort() 排序, 這樣雖然不會有錯誤, 但實際上按欄位標題卻沒有動作 :

            query=m.Visitors.all()
            query.filter(search_field + " >= ", search_what)
            query.filter(search_field + " < ", search_what + u'\ufffd')
            if order=="desc":
                query.order("-" + search_field)
            else:
                query.order(search_field)

總之, 測試到目前為止, 結論是有搜尋功能就沒有欄位排序功能, 反之亦然.

我覺得很奇怪, SQL 的 LIKE 語法在 MySQL, SQL Server, 甚至微軟的 ACCESS 都能很方便地被用做對特定欄位的全文檢索, 不知道為何 Google 的 Big Table 就是不能提供? 只能以很彆扭的方式勉強做到 LIKE "search_what%"?

2016-01-11 補充 :

後來我在下面這篇看到, StringList 欄位如果用 "=" 去搜尋, 事實上是 LIKE "%search_what%" 的功能, 亦即可以做全文檢索 :

在GAE中模拟like查询进行模糊搜索

所以我又增加了下面測試 6. 在此測試中, 我想對 ip 與 user_agent 兩個欄位做全文檢索, 因此在 model.py 中增加了 ip_list 與 user_agent_list 這兩個欄位 :

# -*- coding: utf-8 -*-
from google.appengine.ext import db

class Members(db.Model):
    account=db.StringProperty()
    password=db.StringProperty()

class Visitors(db.Model):
    ip=db.StringProperty()
    ip_list=db.StringListProperty()
    visit_time=db.DateTimeProperty()
    user_agent=db.StringProperty()
    user_agent_list=db.StringListProperty()

member=Members(account="admin",password="aaa")
member.put()
member=Members(account="guest",password="guest")
member.put()

然後在 templates 下複製 list_visitors_5.htm 為 list_visitors_6.htm, 但僅修改搜尋用的下拉式選單 search_field 元件的值, 將原來的 ip 改為搜尋 ip_list 欄位, 原來的 user_agent 改為搜尋 user_agent_list 欄位 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="visitors" title="訪客列表" style="width:960px;" toolbar="#search_bar"></table>
  <div id="search_bar" style="padding:3px">
    <select id="search_field">
      <option value="ip_list">IP 位址</option>
      <option value="visit_time">到訪時間</option>
      <option value="user_agent_list">瀏覽器</option>
    </select>
    <input id="search_what" style="line-height:15px;border:1px solid #ccc">
    <a href="#" class="easyui-linkbutton" iconCls="icon-search" onclick="doSearch()">搜尋</a>
  </div>
  <script language="javascript">
    $(document).ready(function(){
      $('#visitors').datagrid({
        url:'/get_visitors_6',
        method:"post",
        columns:[[
          {field:'ip',title:'IP 位址',width:150,sortable:true},
          {field:'visit_time',title:'到訪時間',width:150,sortable:true},
          {field:'user_agent',title:'瀏覽器',sortable:true}
          ]],
        singleSelect:true,
        collapsible:true,
        rownumbers:true,
        pagination:true,
        pageSize:10
        });
      });
    function doSearch(){
      $('#visitors').datagrid('load',{
        search_field: $('#search_field').val(),
        search_what: $('#search_what').val()
        });
      }
  </script>
{% endblock%}

最後在 main.py 中增加 list_visitors_6 與 get_visitors_6 這兩個路徑與其處理類別如下 :

class list_visitors_6(webapp2.RequestHandler):
    def get(self):
        url="templates/list_visitors_6.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{}) 
        self.response.out.write(content)

class get_visitors_6(webapp2.RequestHandler):
    def post(self):
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        search_field=self.request.get("search_field")
        search_what=self.request.get("search_what")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="visit_time"
        if not len(order):
            order="desc"
        if len(search_field):
            if search_field=="ip_list":
                query=m.Visitors.gql("WHERE ip_list = :1", list(search_what))
            elif search_field=="user_agent_list":
                query=m.Visitors.gql("WHERE user_agent_list = :1", list(search_what))
            else:
                query=m.Visitors.all()
                query.filter(search_field + " >= ", search_what)
                query.filter(search_field + " < ", search_what + u'\ufffd') 
        else:
            query=m.Visitors.gql("ORDER BY %s %s" % (sort, order))
        visitors=query.fetch(rows, (page-1)*rows)
        rows=[]
        for v in visitors:
            visit_time=v.visit_time.strftime("%Y-%m-%d %H:%M:%S")
            visitor={"ip":v.ip,
                     "visit_time":visit_time,
                     "user_agent":v.user_agent}
            rows.append(visitor)
        count=query.count()
        obj={"total":count,"rows":rows}
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj)) 

class MainHandler(webapp2.RequestHandler):
    def get(self):
        url="templates/default.htm"
        ip=self.request.remote_addr
        user_agent=os.environ.get("HTTP_USER_AGENT")
        #user_agent=self.request.headers.get("User-Agent")
        visitor=m.Visitors()
        visitor.ip=ip
        visitor.visit_time=datetime.now() + timedelta(hours=+8)
        visitor.user_agent=user_agent
        visitor.ip_list=list(ip)
        visitor.user_agent_list=list(user_agent)
        visitor.put()
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4),
    ('/google_user_login_5', google_user_login_5),
    ('/list_visitors_1', list_visitors_1),
    ('/list_visitors_2', list_visitors_2),
    ('/get_visitors_2', get_visitors_2),
    ('/list_visitors_3', list_visitors_3),
    ('/get_visitors_3', get_visitors_3),
    ('/list_visitors_4', list_visitors_4),
    ('/get_visitors_4', get_visitors_4),
    ('/list_visitors_5', list_visitors_5),
    ('/get_visitors_5', get_visitors_5),
    ('/list_visitors_6', list_visitors_6),
    ('/get_visitors_6', get_visitors_6)
], debug=True)

首先我們須修改根目錄處理類別, 當使用者要求根目錄時, 把 ip 與 user_agent 這兩個變數用 list() 函式轉成串列型態後存入 ip_list 與 user_agnet_list 欄位中去, 這樣才能對此二欄位進行全文檢索. 

其次是在 get_visitors_6 類別中, 我們須判斷 search_field 之值, 當其為 "ip_list" 或 "user_agent_list" 時, 就用 "=" 判斷去搜尋這兩個欄位. 注意, 因為此兩個欄位儲存的是 list (串列) 資料, 因此 search_what 也必須用 list() 函式轉成串列型態才行. 實際測試範例如下 :


結果真的可以對 ip_list 與 user_agent_list 欄位進行全文檢索, 不過代價資料儲存要花兩倍的空間. 我連線到 GAE 的 Datastore 管理頁面去檢視資料實體, 發現用 list() 函式將字串轉成串列時, 它其實是把字串的每個字元拆開來存, 例如 ip 欄位值為 223.141.225.199, 它的 ip_list 欄位值如下 :

["2","2","3",".","1","4","1",".","2","2","5",".","1","9","9"]

而 user_agent 欄位字串較長, 例如 :

Mozilla/5.0 (Windows NT 6.3; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0

其相對的 user_agent_list 欄位值就很長了 :
["M","o","z","i","l","l","a","/","5",".","0"," ","(","W","i","n","d","o","w","s"," ","N","T"," ","6",".","3",";"," ","W","O","W","6","4",";"," ","r","v",":","4","3",".","0",")"," ","G","e","c","k","o","/","2","0","1","0","0","1","0","1"," ","F","i","r","e","f","o","x","/","4","3",".","0"]

這樣拆成單一字元儲存, 然後用 "=" 去匹配就能做全文檢索, 其機制為何? 我也只知其然不知其所以然. 不過實際測試後發現, 它應該是把要搜尋的字元拆開後, 去 StringList 欄位中搜尋是否含有這些字元, 所以嚴格來講不是真正的 LIKE "%search_what%" 模糊搜尋, 因為會找到比預期還多的東西, 例如我搜尋 IP 位址含有 21 者找到 4 筆, 其中的 223.114.225.199 並沒有 21 字串, 但還是被找出來, 我想就是因為裡面含有 2 與 1 的緣故 :


若改為搜尋 217, 就只剩 3 筆, 223.114.225.199 就被刷掉了, 因為它沒有 7 :


所以這種解決方式只是以化整為零的方式將字串打散為字元串列儲存, 搜尋時也是把欲搜尋的字串打散, 然後去儲存的串列中找看看是否都含有這些字元罷了. 雖然要找的都有找到, 但這不是肯德基!

好了, 關於 GAE 上 EasyUI Datagrid 的前後端互動大概如此, GAE 在資料庫方面不如 PHP+MySQL 那樣方便好用, 像欄位全文檢索這麼稀鬆平常的功能在 GAE 竟然如此彆扭. 或許真的是有一好沒兩好, Google 雲端穩定快速可擴充性佳的代價就是, 程式師要辛苦點. 

以上測試的全部原始碼可在下列網址下載 :