2016年11月28日 星期一

2016 年第 46 周記事

時序已過了 46 周了, 再過五周這一年就要過去, 如果不是有認真在寫周記還真的搞不清楚日子是怎樣過去的. 本周比較重要的是周二去上了兩天 Python 進階的課程, 把我的注意力從 Arduino 導回 Python 以及 GAE, 主要是從課堂上得知兩本 Python 在資料擷取方面的好書, 我就想說應該可以用樹莓派 Linux 裡的 cron table 來抓資料經過分析後存在 MySQL 資料庫裡, 然後將分析結果以 Email 傳給手機. 等有空再次玩樹莓派時再來試試看.

這次去上課巧遇了兩個人, 一個是同棟不同單位但已調職他處多年的美智, 我們彼此都忘了對方名字了; 另外一位是以前跟我一起奉派去美國舊金山研習的三元兄, 算算這竟然已經是 11 年前的事了, 日子怎麼過得這麼快呀! 可不是嗎? 當時二哥才上中班呢! 轉眼已經上了高中. 我覺得五十歲之後, 對時光越來越來越敏感, 恨不得時間的巨輪能轉慢一些, 對於人生的得失也較了然, 比較不想去計較了.

很久沒在管菜園, 上週日鋤了一小塊地來種玉米, 玉米用的是年初小舅給我的一支玉米棒, 但是沒把握會順利發芽, 所以旁邊與重新栽植了地瓜葉. 一周過去了, 似乎沒看到發芽跡象, 周日一整天都在下雨, 我看再不發芽可能都要爛掉了, 下周乾脆去買玉米苗來種算了.

另外就是 11/23 向英國 Hostinger 以美金 27 元租用其 Premium 虛擬主機三年 (到 2019/11/23), 這幾天陸續將專案上傳部署後, 設定 Advanced Cron Jobs 功能去觸發指定之 PHP 網路爬蟲程式擷取資訊後進行分析, 平均執行時間約在 30~40 秒完成, 我對此 Advanced Cron Jobs 功能相當滿意, 因為它最快可以設定每分鐘觸發一次.


本周菁菁與二哥本來要跟我一起回鄉下, 但理化家教黃老師周六下午有事, 問說可否改為周日下午; 而二哥則補習班臨時要補課, 所以仍是我一人回鄉下. 以後上大學或就業, 全家一起回鄉下的機會想必是越來越少了. 從二哥國二去全晟補習理化開始, 大家坐 QRV 回鄉下的情景就變成我跟姊姊與菁菁三人而已. 然後今年七月姐姐也為了準備明年的學測開始週日要去補習班, 就變成我跟菁菁兩人了. 接下來呢, 菁菁也要面臨會考了, 小舅媽說得沒錯, 以後會變成只有我一個人回去了. 雖然明白這是離巢期將近的訊息, 但 ... 孤寂總是需要些時間來調適 ... Good old days.


2016年11月26日 星期六

Hostinger 的 Advanced Cron Jobs 設定

昨天終於有空將我的 PHP 專案上傳到 Hostinger 虛擬主機, 安裝好應用程式後馬上就來測試它的 "Advanced Cron Jobs" 功能, 如果好用的話就可以自行觸發 PHP 程式的執行, 不用再依靠 GAE 的 Cron  Jobs 從外部查詢觸發了.

首先登入後台管理頁面, 往下移到 "Advance" 區塊按 "Advanced Cron Jobs" 鈕 :



最上面 "Command to run" 框是填入要執行的 PHP 程式, 裡面有提示預設格式如下 :

/usr/bin/php /home/u123456789/public_html/cron.php

注意喔, php 與後面的 / 之間必須空一格, 我第一次輸入就忽略這個, 雖然可以順利建立一個 cron, 但是在 View Output 時就會看到下列 "Not a directory" 的錯誤訊息 :

/bin/sh: /usr/bin/php/home/u123456789/public_html/cron/fetch_twse_daily_close.php: Not a directory

我猜這是 Hostinger 當初在 /user/bin 下建立 php 目錄時多敲了一個空格所致, 可能用戶多了之後才發現, 只好將錯就錯了. 我的 cron jobs 檔都放在根目錄的 cron 資料夾下面, 所以正確的路徑應該是 :

/usr/bin/php /home/u123456789/public_html/cron/fetch_twse_daily_close.php



這樣 cron tables 便設定完成, 按 "View Output" 可看到最近一次執行結果 (如果沒有的話, 可能是被瀏覽器擋掉了, 檢查網址列是否有顯示自動阻擋彈出式選單的統計, 將其取消阻擋即可). 與 "Cron Jobs" (Simple) 比較, "Advanced Cron Jobs" 最快執行頻率可設到每分鐘跑一次, 設定選項比較細比較多; 而 "Cron Jobs" 則最快是一小時跑一次, 而且只能在整點時跑 (即 0 分時), 選項也比較少.


"Cron Jobs" (Simple) 的 command to run 欄位已經預先固定路徑到我的帳號底下, 因此不會碰到 "Advanced Cron Jobs" 中 php 後面須有一空格問題.

不過, 不論是 "Advanced Cron Jobs" 或 "Cron Jobs", 同一個程式只能設一次, 但如果分別在兩邊各設一次則可以, 亦即兩者是各自獨立的. 要解決此問題也很簡單, 就是把要在不同時間執行的程式複製為另一個檔案, 例如 fetch_twse_daily_1.php 與 fetch_twse_daily_2.php 即可, 或者乾脆就每個小時執行一次也不為過, 有些意天只能產生一筆的資料就在程式中用日期欄位來判斷即可.

當然我還是可以從外部用 GAE 來觸發執行, 但先看看內部觸發跑的情況如何, 優的話就不必用到 GAE 了.

2016-11-29 補充 :

觀察 Cron jobs 運轉兩天的報告, 我發現指定時間執行的程式都比預期時間晚 8 個小時執行, 例如指定 16:00:00 執行, 實際上卻是到 00:00:00 才真的跑. 我猜這是因為台灣與英國有 8 小時時差的關係, 我們是 GMT+8, 所以若指定下午四點執行, 我們下午四點時英國才當日早上 8 點, 而英國下午 4 點時我們已是晚上 12 點. 我已寫信詢問 Hostinger 是否有地方可以設定 Cron Jobs 時區的? 不過我相信是沒有, 這部分可能得自己換算, 反正就是將我們預定要執行的時間減 8 即可, 要不然就改為每小時或每兩小時執行一次, 然後在程式中依據日期欄位判斷是否已經執行過了, 是的話跳過即可.


河堤公園單車行

早上載菁菁去圖書館回來後, 看到我的小白已經兩個禮拜沒騎了 (太忙了), 還好手機有帶, 就直接換小白騎出去, 今天主要是想測量沿河堤公園騎到博愛二路與三路兩端來回路程與時間. 路線圖如下 :


我從明誠路出發經大順路到博愛二路的愛河之心折返, 再騎回明誠路往前過新莊仔路, 自由路, 再到博愛三路折返明誠路, 這樣總計是 7.38 公里, 耗時約 40 分鐘 :


這樣單程應該是 3.6 公里. 每周都要這樣鍛鍊 1~2 次, 加上慢跑與游泳, 希望在冬季結束前完成南迴段, 可能的話也把 20 多年前停下來的彰化~嘉義段, 嘉義~高雄段補齊, 這樣就只剩下花東段了.

2016年11月25日 星期五

下載備份 Google App Engine 應用程式檔案的新方式

之前我在 Hostinger 上申請的免費網站由於進行了大量運算被認為濫用免費資源而遭到停權 (我哪知道免費用戶的 CPU 使用量是多少啊), 結果促使我在前天向 Hostinger 購買了 Premium 虛擬主機服務. 但是我發現原先的免費帳號還是不斷地被之前設定的 GAE cron jobs 進行 query, 因此在寫信向 Hostinger 解釋之前必須先停止 GAE 上的 cron jobs 才行.

我參考了之前的文章去下載 GAE 應用程式以便檢查看看到底是哪一個應用程式在 trigger 我的 Hostinger 免費主機, 因為太久忘記了, 所以就從最接近的 appfog-twstockbot 開始查起 :

# 下載備份 Google App Engine 應用程式檔案的方法

但是照原先的方法下載卻發現不像以前那樣會在 DOS 命令列中直接詢問 Google 帳號密碼, 而是顯示如下訊息 :

Microsoft Windows [版本 10.0.14393]
(c) 2016 Microsoft Corporation. 著作權所有,並保留一切權利。

E:\>appcfg.py download_app -A appfog-twstockbot -V 1 E:\gae\appfog-twstockbot
09:35 PM Host: appengine.google.com
09:35 PM Fetching file list...
Your browser has been opened to visit:

    https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fappengine.admin+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&response_type=code&client_id=550516889913.apps.googleusercontent.com&access_type=offline

If your browser is on a different machine then exit and re-run this
application with the command-line parameter

  --noauth_local_webserver

這時 DOS 就僵在那邊無法動彈 (其實就是在等待我們使用瀏覽器進行 Google 帳號認證), 同時冒出一個視窗詢問要用哪一個瀏覽器認證, 選好後就會開啟瀏覽器連線到 Google 進行認證, 我的 Google 帳號採用兩階段認證, 因此必須用登記的手機接收認證碼, 驗證成功後會顯示要求授權存取 GAE 與 Google Cloud 頁面 :


按允許後結束認證, DOS 畫面即回應開始下載 :

Authentication successful. (認證通過)
09:36 PM Fetching files...
09:36 PM [1/10] main.py (這個檔案就是我要檢查的 cron table 觸發對象)
09:36 PM [2/10] extjs.htm
09:36 PM [3/10] jquery.htm
09:36 PM [4/10] index.yaml
09:36 PM [5/10] static/dataTables.zh-tw.txt
09:36 PM [6/10] html5.htm
09:36 PM [7/10] app.yaml
09:36 PM [8/10] cron.yaml (這是 cron table)
09:36 PM [9/10] default.htm
09:36 PM [10/10] favicon.ico

打開 main.py (此為 MTV 架構中的 View, 或 MCV 架構中的 Controller, 負責 routing) 檢查, 果然就是去觸發我在 Hostinger 申請的免費的 16mb.com 網址, 抓到了. 以前如果佈署完應用程式就馬上做紀錄的話, 現在也不會這麼費事去調查.

接著檢查其中的 cron.yaml, 其內容就是 cron table :

cron:
- description: fetch_twse_daily_close
  url: /fetch_twse_daily_close
  schedule: every day 16:00
  timezone: Asia/Taipei
- description: fetch_twse_daily_institutes
  url: /fetch_twse_daily_institutes
  schedule: every day 16:02
  timezone: Asia/Taipei
- description: technical_analysis
  url: /technical_analysis
  schedule: every day 16:04
  timezone: Asia/Taipei
- description: create_daily_report
  url: /create_daily_report
  schedule: every day 16:08
  timezone: Asia/Taipei
- description: fetch_daily_report
  url: /fetch_daily_report
  schedule: every day 16:10
  timezone: Asia/Taipei
- description: fetch_twse_daily_debit
  url: /fetch_twse_daily_debit
  schedule: every day 21:00
  timezone: Asia/Taipei
- description: fetch_2330_tw_basic
  url: /fetch_2330_tw_basic
  schedule: every 11 minutes
  timezone: Asia/Taipei
- description: fetch_2330_tw_dividend
  url: /fetch_2330_tw_dividend
  schedule: every 22 minutes
  timezone: Asia/Taipei
- description: fetch_2330_tw_analysis
  url: /fetch_2330_tw_analysis
  schedule: every 12 minutes
  timezone: Asia/Taipei
- description: fetch_2330_tw_balance
  url: /fetch_2330_tw_balance
  schedule: every 13 minutes
  timezone: Asia/Taipei
- description: fetch_2330_tw_cash
  url: /fetch_2330_tw_cash
  schedule: every 14 minutes
  timezone: Asia/Taipei
- description: fetch_2330_tw_income
  url: /fetch_2330_tw_income
  schedule: every 15 minutes
  timezone: Asia/Taipei
- description: fetch_2330_tw_profit
  url: /fetch_2330_tw_profit
  schedule: every 16 minutes
  timezone: Asia/Taipei
- description: fetch_2330_tw_owner
  url: /fetch_2330_tw_owner
  schedule: every 21 minutes
  timezone: Asia/Taipei
- description: fetch_yahoo_company
  url: /fetch_yahoo_company
  schedule: every 60 minutes
  timezone: Asia/Taipei
- description: fetch_yahoo_dealing
  url: /fetch_yahoo_dealing
  schedule: every 9 minutes
  timezone: Asia/Taipei
- description: fetch_yahoo_credit
  url: /fetch_yahoo_credit
  schedule: every 10 minutes
  timezone: Asia/Taipei
- description: fetch_yahoo_major
  url: /fetch_yahoo_major
  schedule: every 10 minutes
  timezone: Asia/Taipei
- description: fetch_yahoo_earning
  url: /fetch_yahoo_earning
  schedule: every 20 minutes
  timezone: Asia/Taipei

把第二行以下全部刪除, 只留第一行的 cron: 然後更新 cron table 即可終止 cron jobs 了. 參考 :

停止 GAE 的 cron jobs 的方法

接下來下達更新 cron 的指令 :

E:\>cd gae

E:\GAE>appcfg.py update_cron appfog-twstockbot
11:14 PM Host: appengine.google.com
11:14 PM Uploading cron entries.

E:\GAE>

註 : 加上 --oauth2 透過瀏覽器認證 (新版的 GAE SDK 已預設不須加, 較舊的則需要)

E:\GAE>appcfg.py --oauth2 update_cron appfog-twstockbot


注意, 不論是下載/上傳應用程式, 或者更新 cron 必須在應用程式 (此處為 appfog-twstockbot) 的上一層目錄 (此處為 GAE) 下執行, 參考 :

# GAE 的 Cron Job

所以上面下載應用程式應該在 E:\GAE 下比較一致 :

E:\cd GAE
E:\GAE>appcfg.py download_app -A appfog-twstockbot -V 1 appfog-twstockbot

最後補充一點, 如果不要用瀏覽器認證, 要像以前那樣直接在 DOS 輸入帳號密碼, 只要加上一些參數即可 :

appcfg.py download_app -A appfog-twstockbot -V 1 E:\gae\appfog-twstockbot --noauth_local_webserver

不過因為我的 Google 已經採用兩階段認證, 因此不能輸入 Google 密碼, 而是要輸入機器認證專用密碼, 我覺得還是用目前以瀏覽器進行認證較方便.

2016-12-26 補充 :

昨晚停掉 GAE 上的 appfog-twstockbot 應用的 cron 後, 果然就靜下來了 :


這樣就可以寫信給 Hostinger 說我的 16mb.com 免費主機只是做實驗用途, 不會再進行大量運算了, 希望他們可以恢復運作. 其實既然已經購買了 Premium 服務, 我是不需要這個免費主機, 但看到帳號底下有個 abuse 字樣總是不舒服, 而且無效的 cron 不斷運作也是耗損能源, 一切無效的動作都必須停止.

# https://cloud.google.com/appengine/docs/python/download

2016-11-27 補充 :

今天在鄉下老家電腦上用較舊版的 GAE SDK 照上面程序下載, 發現它還是跟以前一樣在 DOS 中詢問 Email 與密碼, 因為我的 Google 帳號已申請兩階段認證, 所以這密碼可不是 Google 帳號的密碼, 而是在 Google 帳號設定中授權 GAE 存取的另一組密碼, 我之前有特別記下來, 但是輸入後卻無法通過認證, 真是傷腦筋, 還要回去找之前所寫的紀錄, 很浪費時間. 我直接在 Google 搜尋, 馬上就找到結果了, 參考這篇 :

# 使用Google App Engine上傳程式, 一直要求輸入帳號密碼, 如何解決?

原來只要加上 --oauth2 就會透過瀏覽器進行 Google 帳號認證了, 例如 :

appcfg.py --oauth2 download_app -A appfog-twstockbot -V 1 E:\GAE\appfog-twstockbot



辦公室的新電腦

周一來了一批新電腦, 我辦公室的那台也在汰換之列. Something new is always good, but~~ 一堆資安要求與軟體重安裝很麻煩. 由於我 D 碟累積了一大堆工作資料, 周一下班前負責幫我調校系統的年輕人說資料搬移至少需要兩小時, 只好丟在那邊讓它跑. 受訓兩天回來才著手安裝軟體, 昨天花了半天才能開始用.

今天開始安裝軟體, Java 還是用 JDK7, 因為之前寫的程式都在此環境跑, 而且我不認為我會用到 Java 8 的那些奇怪的新功能, 也不打算在 Java 上花額外的心力, 我大概只要用 Java 來擷取與分析營運資料即可. 去上兩天 Pyhton 進階班課成後, 我決定專注在 Python 的學習.

安裝完 JDK 7 後要到 "控制台/系統" 下設定環境變數 :

JAVA_HOME=C:\Program Files (x86)\Java\jdk1.7.0_79
PATH= ~ ;%JAVA_HOME%\bin;

這樣在任何目錄下便能順利執行 Java 程式了.


好書 : 系統程式

這本書是從鄉下的市圖分館借來的, 此書寫得極好, 以作者設計的一個簡易 32 位元處理器 CPU0 為對象來說明如何實作其組譯器, 編譯器, 作業系統等系統程式. 看了前面一點便沒有時間看, 還是先歸還, 等以後有空再借, 這裡做個紀錄以便後續追蹤.

# 系統程式 System Software (旗標, 陳鍾誠)


作者陳鍾誠係金門大學教授, 涉獵廣泛著作頗豐, 參考 :

http://ccckmit.wikidot.com


2016年11月24日 星期四

如何取消手機未應答轉接

二哥前陣子問我說他的手機會接到打給我的電話, 我以為是之前在 3G 時代申請的雙號同振沒取消掉, 今天打電話問中華行動客服是否可取消, 結果竟然我沒有申請同振, 客服認為可能是我手機有設定未應答轉接之故, 可以在撥號盤輸入下列號碼後按撥號 :

##002#

按完後再試真的就不會有這現象了, 證實是未應答轉接. 如果要設定未應答轉接到某個號碼例如 0933000000 (假設號碼), 則要輸入如下號碼 :

**61*0933000000#

注意, 藍色部分要換成你要轉去的號碼.


購買 Hostinger Premium 虛擬主機服務

昨晚寫完 Python 學習筆記正想關機睡覺, 突然想到上周評估虛擬主機時有打算以每月 1.99 歐元租用英國 Hostinger 的 Premium 主機, 現在是否還有這個價格? 結果連線過去發現竟然有更好的折扣優惠, 租三年只要 27 歐元, 並贈送 .xyz 域名一年 !



點進去看發現只有租三年才有最高 15 折, 每月 0.75 歐元優惠, 租四年可沒這麼好哩 !


於是我當機立斷, 擇期不如撞期, 就給他下單了 :


可知 Hostinger 雖是 UK 公司, 但卻在塞普路斯註冊. 右上角還註明必須 11/23 日結束前付款, 我下單時雖已過 12 點為 11 月 24 日, 但在塞普路斯還是 11/23. 我使用信用卡付款後馬上收到銀行簡訊通知信用卡消費約 1027 元, 推算匯率是 1027/27=38 元, 哇咧, 比今天台銀收盤價 34 元高出 4 元, 這是手續費嗎? 這個價格以我上周我評估時四年 3333 元來算, 換成三年是 3333*3/4=2500, 則1027 相當於是上周的 41 折 : 1027/2500=0.41.

付款後網頁出現兩項訂單需要設定, 一是虛擬主機, 一是免費一年的域名 :


主機設定主要是輸入域名, 密碼, 以及選擇主機位置, 我選北美 :


而域名設定部分則是輸入姓名住址與電話而已 :


以現有 Hostinger 帳號登入系統後果然出現了這個新的主機, 租期三年到 2019-11-23 :


如果不是這麼湊巧, 我還不知道要猶豫多久才會下決定哩! 

2016-11-24 補充 :

早上登入 Hostinger 時出現下列告示 :

"Your domain is not pointing to our nameservers at the moment, so services such as FTP, File Manager, E-mail (and others) will not work correctly. You can find our nameservers at the "Accounts -> Details" section. Please mind that DNS might take up to 24 hours to propagate when the change is submitted."

好像是說我申請的一年免費 .xyz 域名尚未指到 Hostinger 的域名伺服器, 這要在哪裡指定呢? 還是說因為要 24 小時才會綁定, 所以這只是通知而已? 我已寫信詢問客服了解 (線上客服時間是英國當地 03:00~22:00, 相當於台灣時間 11:00~24:00). Hostinger 的域名伺服器如下 :


Business 主機也有打折優惠 (同樣租三年 15 折), 但我比較了 Premium 與 Business 的差別, 發現兩者只有資料庫備份週期與是否提供 SSL 這兩項不同而已 :


這兩項對我而言並非很重要, 而兩者價格卻差兩倍, 所以我覺得還是 Premium 價划算.

2016-11-24 晚上補充 :

晚上吃過飯後收信發現 Hostinger 通知我的一年免費域名已經開通, 實際連線果然顯示預設內容了, 可見域名啟用大約要花將近一天. 另外登入系統後在管理網頁右下角會顯示線上諮詢按鈕 :


按此按鈕會出現線上對談框, 可以向客服人員提出疑問. 我今天主要是問, 我的免費 .xyz 域名明年到期後若不續用, 可否切換為 Hostinger 提供的免費域名 (例如 16mb.com 的) ?


答案是可以的, 但不是在哪裡設定切換, 而是要按 Home/Hosting 底下的 New Hosting Account 鈕新增一個帳戶, 然後建立一個免費域名來連結虛擬主機即可.

2016-11-25 補充 :

我第一次申請 Hostinger 帳號是在 2016-2-16 日, 所寄 Email 資訊如下 :

Nameserver 1: ns1.hostinger.co.uk 31.170.163.241
Nameserver 2: ns2.hostinger.co.uk 31.220.23.1
Nameserver 3: ns3.hostinger.co.uk 173.192.183.247
Nameserver 4: ns4.hostinger.co.uk 31.170.164.249

Uploading Your Website

You may use one of the addresses given below manage your web site:

Temporary FTP Hostname: 31.220.16.247
Full FTP Hostname: ftp.yourAccount
FTP Username: uxxxxxxxxx
FTP Password: yyyyyyy

You must upload files to the public_html folder!

Email Settings

For email accounts that you setup, you should use the following connection details in your email program:

POP3 Host Address: mx1.hostinger.co.uk
Username: The email address you are checking email for
Password: As specified when setting up the email account

Thank you for choosing us.

Hostinger United Kingdom
http://www.hostinger.co.uk

那時為了 Hostinger 不再支援 mysql 函式庫, 我還特別為此將 mysql.php 改為 mysqli.php, 參考 :

EasyuiCMS 改版為 v2 (mysqli)


2016-12-05 補充 :

這幾天測試 PHP 專案, 發現有時候會出現如下結果 :

Resource Limit Exceeded

The website is temporarily unable to service your request as it exceeded resource limit. Please try again later.

我想這可能是 Premium 只保證 99.9% 穩定度的關係吧.

2016年11月23日 星期三

Python 學習筆記 : 安裝執行環境與 IDLE 基本操作

這兩天被派去受 "Python 進階班" 的訓, 有一段時間沒接觸 Python 了, 趁這個機會溫習一下也好. 老師使用的是 Python 3, 而我以前在 GAE 用的是 Pyhton 2, 但其實差異沒有很多啦. 以下就這兩天的 Python 3 學習做個摘要整理.

Python 是一種跨平台的指令碼語言 (Scripting language), 乃 Python 之父 Guido van Rossum 為了發展 Amoeba 分散式處理作業系統, 於 1989 年以其先前發展之 ABC 教學語言為基礎, 揉合了 modula-3, UNIX shell, Icon, SmallTalk 與 C 語言的風格於荷蘭阿姆斯特丹大學發展而成. Python 語言的基本哲學是 : 做事時應該只有一種明確的方式, 這使得 Python 程式較簡潔易學.

Python 虛擬機器可在多種 OS 上運作, 但最先是在 Mac 機器上實作的. Python 的圖騰是一隻蟒蛇, 但其名稱的由來卻與蟒蛇一點關係也沒有, 而是源自英國 BBC 著名的喜劇影集 "Monty Python’s Flying Circus" 來的. 而其整合開發環境叫做 IDLE 而非 IDE 的原因是 Monty Python 開發小組的一個成員名為 Eric Idle 的緣故.

Python 在 2000-10-16 發布了 Python 2.0, 支援垃圾回收與 Unicode, 此版本是 Python 大受歡迎的開始. 2008-12-03 又發布了 Python 3.0, 但與舊版本不完全相容 (not backward-compatible), 這在程式語言演進上較為罕見. 關於 Python 2 與 Python 3 的差異可參考這篇 :

# Python 2.x 或 3.x?

Python 語言的特色整理如下 :
  1. Python 是物件導向的直譯式語言 (Interpreter language), 與 Java/C++ 等編譯語言比起來, 省掉了編譯與連結步驟, 簡化了開發流程, 但事實上是把中介碼 byte code 的編譯動作隱藏起來了.
  2. Python 與 Javascript, PHP 等語言一樣均屬於腳本語言 (Scripting language), 其與系統語言 (C/C++) 之差異是 : 系統語言目標是節省系統資源, 提高執行效能 (資訊家電需求), 而腳本語言之目標是節省程式員時間, 提高開發速度 (應用程式需求).
  3. Python 是動態語言 (Dynamic language), 在執行時期才確定資料型別, 這與 Javascript, VBscript, Ruby 等語言是相同的.
  4. Python 是強型別語言 (Strong-typed language), 資料型別須透過顯式型別轉換才能混合運算, 如同 Java.
  5. Python 不是格式自由語言, 採用強制縮排定義程式區塊, 以換行表示敘述結束. 縮排是語法的一部分, 違反縮排規則將無法執行.
  6. Python 是跨平台語言, 早期本身是用 ANSI C 語言撰寫的, 可以在 Unix/Linux/DOS/Windows/Macintosh 等作業系統上執行. Python 程式執行時會編譯成 pycode 中介碼, 與 Java 的 bytecode 類似.
  7. Python 是開放源碼 (Open source) 的.
  8. Python 內建比 Java/C/C++ 還豐富的資料型別, 從而有效地減少程式碼長度.
  9. Python 內建複數型別, 支援複數運算.
  10. Python 是完全物件導向的語言, 不論資料, 字串, 函式, 模組全部都是物件. 完全支援多重繼承, 多型, 過載, 衍生, 與泛型, 使程式碼可重複使用. 同時也有例外處理機制, 具備垃圾回收功能, 自動管理記憶體之使用. 
  11. 利用 py2exe, pypy, pyinstaller 可將 Python 原始碼轉成脫離編譯器執行環境的獨立執行程式.
  12. Pyhton 的設計哲學 : 優雅, 明確, 簡單. 希望 "用一種方法, 最好是只有一種方法來做一件事", 而非像 Perl 那樣 "有多種方法來做同一件事", 盡量避免歧義, 因此 Python 原始碼具有高度的可讀性.
  13. Python 雖然被歸類為 "指令碼/描述語言 (Scripting language)", 但其功能遠比 Shell script, Javascript, VBscript 等僅能處理簡單工作之指令碼語言強大, Python 開發者稱其為一種 "高階動態語言".
  14. Python 也被稱為 Glue language (膠合語言), 能夠將其他語言編寫的程式進行整合與封裝.
  15. 大多數 Linux 發行版本都已整合了 Python, 可以在終端機下直接執行 Python.
  16. Python 語言的架構是可延伸的, 並非所有的功能與特性都整合到核心, 而是歸類於其強大的標準函式庫中, 從而避免像 VBscript 那樣臃腫. Python 輕巧的語言核心只包括數字, 字串, 串列, 字典, 檔案等資料型別與函式, 其他額外功能如系統管理, 網路通訊, 文字處理, 資料庫, 圖形介面, XML 等全部由標準函式庫提供. 另外社群還提供豐富的套件, 如 Web 開發, 科學計算等等.
  17. Python 標準函式庫包含多個作業系統功能呼叫函式庫, 透過 pywin32 套件可以存取 Windows API 與 COM 服務; 而透過 IronPython 則可直接呼叫 .Net Framework.
  18. 以 Python 編寫系統管理指令在可讀性, 效能, 重覆使用, 擴充性方面都優於 shell 指令.
  19. Python 已有的實作 : CPython (C), JPython (Java), PyPy (Python), IronPyhon (.Net).
以上都是關於 Python 的背景知識, 現在 Python 越來越受歡迎, 不僅在科學計算領域被廣泛使用, 許多大學如 MIT 與柏克萊等校之資訊科學系也改用 Python 作為學生必修語言, 目前 Python 在 TIOBE 排行已經穩定在前五名 :


自 Python 1.0 於 1994 年問世以來, 過去 22 年來 Pyhton 的排名從 26 名穩定上升到目前的第五名, 其受歡迎的程度可見一斑 :


此外在嵌入式系統應用中, Python 也是樹莓派 Raspberry Pi 的預設作業系統 Raspbian 的內建程式語言, 樹莓派也因為 Python 龐大的第三方套件而展現驚人的功能, 廣泛應用在無人機, 機器人, 影像辨識, 自然語言處理等領域. Python 的厲害在於其豐富的第三方套件, 在 PyPi 網站上已登錄超過 9 萬個套件, 各種應用應有盡有, 參考 :

https://pypi.python.org/pypi

學習 Python 要先安裝其執行環境, 首先到官網下載 Python :

https://www.python.org/downloads/

目前最新版是 3.5.2, 如果是用在 GAE 的話要下載 2.7 版, 因為 GAE 尚未支援 Python 3.  Python 2.7 版也是 Python 2 的最後版本.

安裝 Python 時要注意更改安裝路徑以及勾選 "Add Python 3.5 to PATH" :


注意, 這裡要選 "Customerized installation", 這樣才能更改安裝路徑 :


這個只要按 Next 即可 :


這裡就是修改安裝路徑的地方, 將原先又臭又長的路徑改為 C:\Python35 即可. 這樣安裝完後在 DOS 視窗的任何目錄下打 Python 都能執行 Python 程式.



直接輸入 Python 指令馬上即輸出結果. 例如 :

>>> 2+3
5
>>> print(2+3)
5

如果要跳出 Python 直譯器, 可輸入 exit() 或 quit() 或按 Ctrl+C 即回到 DOS 命令列.

此外也可以使用 Python 內建的 IDLE 交談式介面來執行 Python 程式, 其功能比由 DOS 進入 Python 執意環境要多, 例如可以編輯 Python 程式後點選 Run Module 來執行檔案.

當下了一些指令後, IDLE 視窗就會塞滿指令與回應, 能不能像在 DOS 一樣下個 CLS 指令就清除畫面呢? 很遺憾的是 IDLE 並沒有這樣的單一指令. 不過可以照下列文章介紹的方法為 IDLE 添加清除畫面快捷鍵 :

IDLE如何清屏?

先下載 ClearWindow.py 程式, 將其放在 Python 3 安裝目錄的 C:\Python35\Lib\idlelib 下, 然後用記事本編輯此目錄下的 config-extensions.def 檔, 將下列指令複製到檔尾後存檔, 將 IDLE 重開即可在 Option 下看到新增的 "Clear Shell Window" 功能選項了, 按此可清除畫面 :



這次受訓從老師那邊發現一本 Python 好書 :

Python 程式設計 : 從初學到活用 Python 開發技巧的 16 堂課 (何敏煌, 博碩)

Source : 天瓏

今天晚上就跑去明儀買回來, 打算好好研讀一番.

參考 :

Python 2.x或3.x?
Python Tutorial 第二堂(1)數值與字串型態

Python 3 與 Python 2 主要的差異如下 :

1. Input 輸入 :

在 Python 2 時可以使用 raw_input() 函數與 input() 函數讀取標準輸入, input() 事實上是透過 raw_input() 實作的, 兩者的傳回值型態不同, raw_input() 傳回值一律是字串, 且字串輸入時不用加引號, 例如 :

>>> raw_input("Name:")
Name:tony     (字串不須加引號)
'tony'
>>> raw_input("Age:")
Age:50
'50'                 (傳回字串)

可見 raw_input() 輸入時都不用加引號. 而 input() 則字串須加引號 :

>>> input("Name:")
Name:tony    (字串須加引號)

Traceback (most recent call last):
  File "    input("Name:")
  File "NameError: name 'tony' is not defined
>>> input("Name:")
Name:"tony"   (字串須加引號)
'tony'
>>> input("Name:")
Name:"tony"
'tony'
>>> input("Age:")
Age:50   (數字不須加引號)
50

可見 input() 只有數字不須加引號.

不過在 Python 3.2.3 版後已經不能用 raw_input() 了, 只能使用 input().

參考 :

# python 研究-raw_input 和 input 差異比較

2. Print 輸出 :

在 Python 2 時 print 是一個敘述 (指令), 列入關鍵字裡面, 使用方式如下 :

print "Hello World"

但是到了 Python 3 時 print 變成函數, 輸出資訊必須放在小括號內 :

print ("Hello World")

3. 中文支援 :

Python 2 雖然也支援中文, 但必須在第一行指定 UTF-8 字元編碼 :

# -*- coding:UTF-8 -*-

同時 .py 檔存檔時必須另存為 UTF-8 編碼格式.

但是在 Python 3 已經全面支援中文了 (預設使用 UTF-8 編碼), 連變數名稱也可以使用中文, 例如 :

>>> 哈囉世界="Hello World"
>>> 哈囉世界
'Hello World'

第一行不用再加 encoding 註記, 但仍要存成 UTF-8 格式.


另外, Python 支援大整數也是一個亮點, 在 C/Java 等語言裡, 整數 int 使用 4 個 bytes 儲存, 資料範圍 -2147483648~2147483647, 超出此範圍即發生溢位錯誤而無法儲存. 在 Python 無此問題, 再大的整數都可以處理. 在 Python 2 版還有分整數 int 與長整數 long, 大整數可以在後面加個 L 標示為長整數. 而且 2.2 版後若發生溢位會自動轉換成長整數, 不會出現執行錯誤. 到 Python 3 之後已經沒有 long 這種資料類型, 整數就只有 int 這個類型, 這個 int 其實就是 2 版的 long, 亦即在 Python 3 裡全部都是長整數.

不過, Python 對大整數的支援無上限指的是直到機器的極限, 看 CPU 是 32/64 位元而定. 而浮點數也是有機器限制的, 32 位元電腦只能表示到小數點後 12 位; 64 位元則至 16 位,  例如 :

>>> 1.0/3.0     (64 位元機器)
0.3333333333333333

而科學表示法可以處理到 1.7e308, 到 1.8e308 就變成 inf (無限大) 了 :

>>> 1.8e308
inf
>>> 1.7e308
1.7e+308
>>>

2017-04-11 補充 :

Python 3 與 Python 2 到底有哪些不同? 參見 :

# https://docs.python.org/3/whatsnew/3.0.html

高應大圖書調借方式

因為這兩天去上 Python 進階班的關係, 到我的第一母校高應大圖書館找 Python 的書, 發現燕巢校區有一本江良志翻譯的 Google App Engine 的書 :

雲端網頁程式設計 : Google App Engine使用Python / Massimiliano Pippi著 ; 江良志譯
http://www.books.com.tw/products/0010676446

因為 GAE 納入 Google Cloud Platform 後感覺有點陌生了, 所以想借回來看看.


但是跨校區必須用借調方式, 不能點預約或 Request1 超連結. 首先到圖書館首頁右下角點圖書借調服務系統 :


先點一下 "個人專區", 輸入帳號密碼登入系統 :


搜尋要找的書 :


按 "申請借調" 即可 :


每次都會忘記還要打電話問櫃台學妹很麻煩, 所以做一下紀錄.

2016-11-24 補充 :

今晚去圖書館取回這本書了, 果然是本難得的好書, 有專家指引可以省掉不少摸索的時間. 等有心得再做紀錄.



2016年11月22日 星期二

2016 年第 45 周記事

本周三四五菁菁去江南渡假村參加學校國二露營, 回來曬成黑美人, 坐實了班上同學給她的原住民外號 (其實我家祖上應該也是有原住民血統). 菁周五一回來馬上又跟水某搭高鐵到桃園開會, 她是去兜風的, 跟小姨子去了貓村玩.

我則周四周五請假跟仲仔他們一夥去攀登合歡山東峰與主峰, 據說是最好爬的百岳. 我的高山行處女秀雖然因輕微高山症 (失眠) 而減分, 但也無意中讓我對爬百岳有了較正確的認識. 不過回來後看了一些山野傳奇卻讓我感到有點懼怕, 對未來可能的高山行造成一些阻力.

週日石岡的小舅公娶媳婦, 雖然新郎新娘差我近 20 多歲, 我還是得乖乖地叫他們一聲叔叔嬸嬸哩. 這是因為祖母的養父娶二房的關係, 小舅公是二房最小的, 所以雖然他年齡遠比爸小, 爸照輩分須叫他舅舅. 這在過去農村大家庭很常見, 那時兄弟姊妹十個以上所在多有, 造成許多帶著比自己年幼的舅舅, 阿姨, 或叔叔去上學的有趣現象. 跟我同桌的朱氏族親有一位小妹妹跟奶奶一起來吃喜酒, 奶奶要她叫我叔公, 我掐指一算, 照輩分我還真的是小妹妹的叔公哩! 真是一種輩分兩樣情啊!


2016年11月16日 星期三

合歡山雲海自由行

明天請假兩天, 跟老同學仲仔, 惠峰, 文壬去合歡山賞雲海. 仲仔傳來的行程表 :

2016 合歡山雲海自由行

11/17(四):
(08:00) 高雄
(09:20) 古坑休息站(休息15min)
(11:25) 清境農場(午餐30min)
(12:10) 松雪樓
(13:30) 合歡東峰
(14:50) 松雪樓(休息10min)-Check in
(15:00) 松雪樓至武嶺
(16:30) 合歡主峰(夕陽)
(18:00) 松雪樓晚餐
(20:00) 星空夜語

11/18(五):
(04:30) 起床
(05:00) 出發
(05:40) 石門山 (日出/雲海)
(07:00) 松雪樓早餐
(08:30) 出發
(09:50) 小奇萊步道休息30min
(11:00) 滑雪山莊
(12:00) 清境農場或埔里午餐(是否至其他地方?)
(15:30) 高雄

攜帶物品:
1. 頭燈或手電筒
2. 禦寒衣物(外套,手套,口罩)(山上氣溫低9~15度)
3. 行進間零食
4. 盥洗用具(因為是環保專案需自備)/ 換洗衣物
5. 小背包+飲用壺(務必裝水)
6. (登山杖)
7. 防滑布鞋-攜帶脫鞋或涼鞋(行進時務必穿布鞋)
8. 遮陽帽/ 防曬乳
9. 身分證件
10.雨具(備用)
11.相機
12.其他個人物品

2016-11-20 補充 :

這次合歡山之行讓我對登山徹底改觀. 以前覺得不過就是背著背包往山裡去有何困難? 上了一趟 3000 公尺以上高山, 才知道待季不是憨人想的哈你甘丹.

這次是仲仔司機兼導遊, 中午到達清境農場吃午餐, 然後直上合歡山松雪樓, 由於要下午三點後才能 checkin, 所以車放松雪樓前面, 先去爬合歡山東峰 (登山口在建築物後面), 下來再 checkin. 這東峰都是木階梯, 才爬一點點就開始喘氣, 挖咧, 體能退步了! 其實是因為高山氧氣較稀薄的關係. 爬到半山腰後就變成走一小段就得休息才行, 因為太喘了.

登合歡東峰中途回望松雪樓與滑雪山莊

好不容易終於到達東峰頂 (標高 3421 公尺) 才發現, 視野真是一級棒, 南邊是以前常在新聞上看到山難消息的奇萊山, 山勢之峻奇雄偉令人感到畏懼. 往西邊清境方向則是層層疊疊的山巒與雲霧, 文壬與惠峰馬上抄起他們的輜重-單眼相機開始取景. 下面這張是利用快門調節拍出來的 (右邊那個是我) :

在合歡東峰施展武功

看起來好像是在東峰上練就了蓋世神功, 哈哈哈.

預計 15:30 要下到松雪樓然後去主峰看夕陽, 但因為在東峰頂待得久了些, 且同學下山走錯小徑又耽擱了些時間, 結果到主峰登山口時太陽只剩下一半, 殘念! 但還是戴上頭燈照計畫兼程趕路於 18:29 左右登上合歡主峰, 隨即下山, 因為要趕松雪樓 19:00 晚餐入席截止時間.

總之第一天就是非常趕, 登山過程一路都非常喘, 有可能是因為這樣讓我晚上在松雪樓整晚都睡不著! 照仲仔計畫, 第二天 04:30 就要起床去登不遠處的石門山, 所以 22:00 就上床就寢了, 但躺下後就聽到心臟砰砰跳, 用小米手環測量心率竟然高達 110 下! 我想該不會晚餐喝了一杯奶茶的緣故吧! 於是半夜起來到門外飲水機喝了兩大杯水企圖沖淡咖啡因濃度, 但是, 還是沒效. 16:30 鬧鐘響起我跟他們說我要補眠, 你們三個去好了. 仲仔說石門山很好爬啦! 我也想萬一早上補眠失敗, 那不就少爬一個百岳了? 但猶豫之後還是試試看能否睡得著. 結果 : 精神亢奮得很!

等他們從石門山回來下去吃早餐時, 聽到一群女生在聊天談到他們晚上也是睡不著, 說第一次登高山很多人都是如此. 轉頭看見電梯口貼了一張關於高山症的告示, 原來失眠頭痛都是輕微高山症的現象! 哇咧我是冤枉昨晚的那杯紅茶了! 難怪即使在平坦的地方走動快一點也會喘哩, 第一次領教了低氧低壓環境的威力!

吃過早餐後仲仔問大家是否照計畫去走小奇萊, 我們三個面面相覷, 惠峰說乾脆打道回府吧! 跟我說登山簡直是自虐! 昨天急行軍氣喘如牛時我也深有同感, 幹嘛這麼累啊! 但仲仔車子開出去後就右轉往底下不遠的滑雪山莊, 停妥之後才知道後面就是奇萊山登山口, 仲仔還是照原計畫要走一趟小奇萊. 這一段長約 1.3 公里的路彎彎曲曲, 上上下下, 原本他說走到不想走就回頭, 哪知走著走著出了箭竹林就到達目的地了. 眼前開闊的視野真令人精神振奮! 對面的黑色奇萊比在合歡山東峰看又更近了.

在小奇萊遠望屏風山與奇萊北峰

在小奇萊停留照相時, 後面又來了一隊約 30 多人的登山隊, 聽他們說明天要去攀登奇萊北峰, 晚上要住在對面山腰的山屋. 我們要趕回高雄就折返松雪樓打道回府了. 我的高山首登秀就在疲累睏乏中結束, 一上車不久就鼾聲大作矣.

回來後對百岳突然有點興趣, 就查了一些資料, 原來登山真是一門大學問, 沒有充分準備與訓練千萬不能貿然登高山. 下面這篇是關於登台灣第二高山雪山的詳盡介紹, 值得參考 :

# 【新手專區】我是新手,我想去雪山!

以下包含一些山野傳奇, 還想登山的勿看 :

奇萊主山
掲開黑色奇萊恐怖真相!2007年 第0097集 2200 關鍵時刻
雪山主峰上墓碑背後的詭異事件?! 2010年 第0855集 2200 關鍵時刻
石門山
就是愛爬山
憶雪山飛狐 kikika
攝影作品賞析80


2016年11月15日 星期二

全球最具性價比全尺寸航拍無人機 HR-SH5W NT$1499?

昨天在臉書看到有人分享的一則空拍機特價訊息 :

# 全球最具性價比全尺寸航拍無人機!台灣特賣!$1499

看網頁中展示的漂亮圖片實在非常心動, 但是搜尋調查此款 SH5W 空拍機卻沒找到多少資料, 除了上面那個網頁外, 只找到 DHGate 群購網站上的兩筆 :

Camera Drones SH5W WiFi FPV 2.4Ghz 4CH 6-Axis Quadcopter Drone HD Camera RC WOct20 $52.52
NEW SH5W Camera Drones UAV Aerial Remote Control Aircraft four aircraft SH5 UAV fall SHRC BEST GIFT DHL$24.23

DHGate 最少須購買 10 件免運費 DHL 由中國直送, 所以我猜這則特價可能是有人下訂後找人分攤, 以最便宜約 $25 美元無攝影機來算, 一台約 800 元, 一台可賺近一倍. 事實上 1499 只是無人機本體加上遙控器而已, 如果加上攝影機與 Wifi 同傳其實要 2399 元 :


但是這網站很奇怪, 重新整理網頁永遠從 9 小時 46 分倒數, 永遠還剩 25 件, 寫信去詢問規格也未回應, 以我多年網購經驗, 實在不敢隨便下單, 上回買菁菁的手機吃過一次虧後我對網購就非常謹慎, 俗話說小心駛得萬年船, 1499 元可以買得到一片 Pi 3 哩. 最重要的是, 從規格看, 充電一小時只能飛 12 分鐘, 這這這, 似乎太少了吧!

2016-11-16 補充 :

今天看到鳥松阿莎力有賣這種迷你空拍機 :

# 阿莎力 24小時內出貨 超迷你四軸飛機 遙控飛機 賣場內五種機型全系列 CX10 CX-10 非直升機空拍機遙控車 $610
# 阿莎力 超迷你遙控飛機 四軸 無頭模式 CX10A CX-10A CX-10升級款 非遙控車空拍機直升機 $680

2016-11-28 補充 :

在我還陷入長考時, 有已購買的網友熱心提供意見, 請看下方留言.



雲端服務 Heroku

Heroku 是一個雲端 PaaS 服務網站, 它一開始是提供免費的 Ruby on Rail 架站服務, 我很早就註冊了, 我那時正開始學習 PHP, 四處找尋免費的 PHP 虛擬主機, 我原以為它可以架 PHP, 後來才搞清楚原來是 RoR. 參考 :

http://yhhuang1966.blogspot.tw/2013/12/heroku.html

我去書店找相關書籍, 發現 RoR 要安裝 Ruby (跟 Appfog 一樣), 使用 command line 下一堆我不熟的指令, 我覺得很麻煩, 為什麼就不提供像 cPanel 那樣方便的網站管理介面呢? 更何況還要學 Ruby 呢! 所以也就沒使用它了.

前陣子在母校高應大借到下面這本介紹 Heroku 架站的書 :

# Heroku 網路應用開發指南 (第二版) (上奇)


因為之前找的一些免費 PHP 虛擬主機都一個個掛點了, 看到這本書突然讓我眼睛一亮, 何不試試看用 Heroku 架站呢? 但是看了前面幾章介紹, 發現用 Heroku 挺麻煩, 還要申請 Github 帳號, 學習如何使用 Github 來寄放與管理專案程式碼, 就只是架個 PHP 網站不是嗎? 幹嘛這麼囉嗦呢? 而且 Heroku 後來好像有新經營者入主, 轉為雲端服務, 免費資源額度似乎不是很大 (5MB 與 750 小時), 我覺得花這麼多功夫後可能是白搭, 決定放棄使用免費資源這個念頭, 免費的可能最貴, 因為要耗掉不少寶貴的時間. 我看還是乖乖付費買 Hostinger 的 Premium 服務好了.

參考 :

# Heroku – 上傳 PHP 網頁
用 Heroku 架設 Wordpress 網站
# [PHP] 使用 Heroku 架設 Wordpress 免費部落格
https://github.com


2016年11月14日 星期一

EasyUI Datagrid 中超連結的編輯問題

這幾天繼續修改 twstockbot 程式, 首先是加寫 trade_records 資料表的部分, 其中我在 inventory 欄位自作聰明地加進超連結, 覺得這樣很方便, 只要按該欄位的 "是", 就透過 ajax 在後端將 "Y" 修改為 "N", 然後重新載入 Datagrid 時就會顯示 "否", 反之亦然.

Datagrid 的顯示內容為超連結在之前測試 EasyUI 的 Datagrid 時有試過, 那時是向外連結到 Yahoo 股市, 單純顯示的話很簡單, 參考下面文章的範例 7-1 與 7-2 :

# jQuery EasyUI 測試 : Datagrid (一)

但是若需要編輯 Datagrid 的紀錄的話, 內容為超連結的欄位會帶來問題, 如下圖所示 :



我覺得這樣很糟糕, 我不能容忍形式上的不完美, 決定去除這個很方便的超連結, 在去除前做一下紀錄, 以便將來在不需要編輯內容的場合要用到點擊超連結這功能時參考.

這個 inventory 欄位的 HTML 部分如下,  就是一個名為 inventory 的 Combobox :

  <div id="trade_records_dialog" class="easyui-dialog" title="新增紀錄" style="width:360px;height:350px;"  data-options="closed:'true',buttons:'#trade_records_buttons'">
    <form id="trade_records_form" style="padding:10px">
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">股票代號 : </label>
        <input id="stock_id" name="stock_id" class="easyui-combobox" data-options="missingMessage:'此欄位為必填',required:true">
      </div>
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">買進價格 : </label>
        <input name="buy_price" class="easyui-numberbox" data-options="missingMessage:'此欄位為必填',required:true" style="width:100px;">
      </div>
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">買進股數 : </label>
        <input name="shares" class="easyui-numberbox" data-options="missingMessage:'此欄位為必填',required:true" value="1000" style="width:100px;">
      </div>
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">買進日期 : </label>
        <input name="buy_date" id="buy_date" type="text" data-options="missingMessage:'此欄位為必填',required:true">
      </div>
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">庫存中 : </label>
        <select name="inventory" class="easyui-combobox" data-options="panelHeight:'auto',missingMessage:'此欄位為必填',required:true" style="width:60px;">
          <option value="Y">是</option>
          <option value="N">否</option>
        </select>
      </div>

      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">賣出日期 : </label>
        <input name="sell_date" id="sell_date" type="text" data-options="">
      </div>
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">賣出價格 : </label>
        <input name="sell_price" class="easyui-numberbox" data-options="" style="width:100px;">
      </div>
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">持有人 : </label>
        <select id="stock_owner" name="stock_owner" class="easyui-combobox" style="width:140px;" data-options="panelHeight:'auto',
        url:'./apps/STOCK.php?op=get_users',
        valueField:'name',
        textField:'name'
        ">
        </select>
        <input type="hidden" id="op" value="">
        <input type="hidden" id="id" value="">
      </div>
      <div style="margin:5px">
        <label style="width:70px;display:inline-block;">備註 : </label>
        <input name="remark" type="text" class="easyui-textbox" style="width:230px">
      </div>
    </form>
  </div>
  <div id="trade_records_buttons" style="padding-right:15px;">
    <a href="#" id="clear_trade_record" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
    <a href="#" id="cancel_trade_record" class="easyui-linkbutton" iconCls="icon-cancel" style="width:90px">取消</a>
    <a href="#" id="save_trade_record" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">確定</a>
  </div>
</div>

關於下拉式選單 ComboBox 的用法參考 :

# Easyui 測試 : Combobox 下拉式選單

而 EasyUI 的 Datagrid 設定如下 :

    $('#trade_records').datagrid({
      columns:[[
        {field:'id',title:'id',sortable:true},
        {field:'stock_id',title:'股號',sortable:true},
        {field:'stock_name',title:'公司'},
        {field:'buy_date',title:'買進日',sortable:true},
        {field:'buy_price',title:'買進價',sortable:true},
        {field:'shares',title:'買進股數',sortable:true},
        {field:'close',title:'收盤價'},
        {field:'book_profit',title:'帳面損益(%)'},
        {field:'inventory',title:'庫存中',sortable:true},
        {field:'sell_price',title:'賣出價',sortable:true},
        {field:'sell_date',title:'賣出日',sortable:true},
        {field:'realized_profit',title:'實現損益(%)'},      
        {field:'stock_owner',title:'持有人',sortable:true}
        ]],
      url:"apps/STOCK.php",
      queryParams:{op:"list_trade_records"},
      method:"post",
      fitColumns:true,
      singleSelect:true,
      pagination:true,
      pageSize:10,
      rownumbers:true
      });

此處收盤價是查詢 stocks_list 表得來的, 兩個損益是計算而來的, 都不在 trade_records資料表中, 都無法在 Datagrid 中參與排序, 因此 sortable 不能設為 true. 

在 op=list_trade_records 程式段落中, 我餵給 Datagrid 中此欄位的是一個超連結 :

        if ($RS[$i]["inventory"]=="N") {
          $inventory="<a href='#' onclick='change_inventory(".
                     '"'.$RS[$i]["id"].'","Y")'."')>否</a>";
          }
        else {
          $inventory="<a href='#' onclick='change_inventory(".
                     '"'.$RS[$i]["id"].'","N")'."')>是</a>";
          }

注意這裡因為要避免標籤屬性值與字串的括號的衝突, 我交互使用了單引號與雙引號, 當然也可以用跳脫字元, 但我很討厭一堆跳脫字元擠在一起的感覺. 超連結的 click 動作處理函數是一個 change_inventory() 函數, 需要傳遞此交易紀錄的 id 與想要改變的狀態值給它, 例如現在是 "否", 那就要傳按下超連結時希望改為 "是" 的 "Y" 給它. 此函數位於 op=trade_records 程式區塊內的最上層 (不是在 jQuery 物件內) :

  function change_inventory(id, inventory){
    $(function(){
      var url="./apps/STOCK.php?op=change_inventory&id=" + id +
              "&inventory=" + inventory;
      $.get(url, function(){
        $("#trade_records").datagrid("reload",{op:"list_trade_records"});
        });
      });
    }

此函數使用 ajax 的 get 方法要求後端執行 op=change_inventory 這個區塊, 完成後重新載入 Datagrid 內容. 這個區塊的內容如下 :

  case "change_inventory" : {
    $id=$_REQUEST["id"];
    $data_array["inventory"]=$_REQUEST["inventory"];
    $result=update("trade_records", $data_array, "id", $id);
    if ($result===TRUE) {
      $status="success";
      $msg="ok";
      }
    else {
      $status="failure";
      $msg="資料更新錯誤!";
      }
    $arr["status"]=$status;
    $arr["msg"]=$msg;
    echo json_encode($arr);
    break;
    }

亦即它只是將 inventory 這個欄位的值從 "Y" 改為 "N" 或 "N" 改為 "Y" 而已. 由於 EasyUI 表單 的 load 方法一定會將 Datagrid 中被編輯的那一列內容載入到表單元素中, 對於 ComboBox 而言, 應該是會載入到 option 元素的 value 屬性中, 於是造成上面那張圖中的怪異現象, 似乎破壞了 HTML 的格式正規性 :

    $("#edit_trade_record").bind("click",function(){
      var row=$("#trade_records").datagrid("getSelected");
      if (row) {
        $("#trade_records_dialog").dialog("open").dialog("setTitle","編輯交易紀錄");
        $("#trade_records_form").form("load",row);
        $("#op").val("update_trade_record");
        $("#id").val(row.id); //edit 需要傳送 id
        }
      else {$.messager.alert("訊息","請選擇要編輯的交易紀錄!","info");}
      });

因為之前沒有實作過這種功能, 記下來以後可能用得到.

2016-11-15 補充 :

下一步 :
加入買入本益比與賣出本益比欄位.
加入總資產資料表 : 保險, 股票, 投資型保單等

2016-11-16 補充 :

今天想測試看看是否 EasyUI 已解決 FireFox 在 ComboBox 無法搜尋中文問題, 將 index.php 與 main.php 各製作了cdn 版與 local 版, 亦即根目錄下多了 index_local.php, index_cdn.php, main_local.php, main_cdn.php 四個檔案, 其中 local 匯入本地自備的 jQuery 與 EasyUI 函式庫; 而 cdn 則匯入 EasyUI 官網最新的 CDN 來源. 要切換時就複製 local 或 cdn 檔去覆蓋 index.php 與 main.php 即可. CDN 環境配置參考 :

jQuery EasyUI 環境配置

使用最新的 CDN 測試結果一樣, 在 FireFox 使用 ComboBox 時仍然因為無法送出中文的 q 參數而無法做中文搜尋, 參考 :

Easyui 測試 : Combobox 下拉式選單


2016 年第 44 周記事

週六 (11/12) 下午 16:30~17:30 成大會計系二年級的黃同學來試教菁菁理化, 下課後又跟他聊了一下, 六點才出發, 上國十又塞車 (可惡的鼎金交流系統的剪刀路!), 回到鄉下老家時已七點了. 本周仍是我一人回鄉下, 因菁菁周日還要練舞 (露營用).

本周我暫停 Arduino 的學習, 回頭複習並繼續我的 PHP 專案了. 有認真考慮要租 Hostinger 的主機來跑我的自動探勘分析程式, 預估每月租金 70 元, 需連租 4 年約 3333 元左右. 用過幾年免費主機, 心得是 : 這種不穩定的打游擊是做不出甚麼成績來的. 

菜園的甘蔗一段時間沒打理它們就恣意生長, 現在已穿越媽以前搭的絲瓜棚, 想重搭棚架都備受制肘, 所以週日中午吃過飯便連砍五根最礙眼的削皮後放在塑膠袋, 讓爸載去水果店榨成汁, 沉甸甸的一大袋竟然只能榨出四瓶甘蔗汁, 砍五根還真是太少了. 

之前原本打算留一次長髮看看是甚麼感覺, 很久以前在台北工作時有試過, 但當時被媽唸到沒辦法最後放棄了. 目下眼見頂上銀髮增多, 而且以後老了可能會像爸那樣成為海灣型禿頭, 所以想留一次看看, 到肩膀純體驗就好. 但這幾天洗頭吹頭發現光是到耳下而已就要花不少時間, 既耗時間又耗電, 最重要的是姊姊與菁菁都說我留長髮沒有以前帥, 所以週日下午決心去理髮, 把已留了快半年的頭髮剪掉. 晚上洗澡時仔細看鏡子, 嘿! 真的帥回來了!


2016年11月11日 星期五

二哥澄清湖露營

昨天二哥他們學校舉辦高一新生露營 (童軍活動), 一連兩天在澄清湖舉行. 下午去鳳山載他, 說非常累, 而且半夜同學搶他睡袋 (他把睡袋當棉被, 難怪會被搶), 好像有點感冒. 回來趕快充一杯蜂蜜檸檬汁喝完就去補眠了.

下周三四五換菁菁她們學校大露營了, 地點在台南江南渡假村. 我則週四周五要請假跟老張他們去合歡山.


EasyUICMS 改版 v3 與虛擬主機評估

今天又是身為勞工的小確幸, 感謝國父, 生日快樂. 昨天晚上將年初 (2/18) Appfog 升版時順便改版的 EasyUICMS 程式再次改版為 v3, 早上把最後的一小部分收尾後上傳雲端保存. 這次改版主要是系統部分將導覽區塊與導覽連結分開, 一些新增編輯對話框加上取消按鈕, 應用程式管理加上顯示內容框等等, 主要是依據年初開發 EasyUICMS on GAE 時留下的備忘錄進行修改, 參考 :

# EasyuiCMS on GAE 總整理

上週日上市集買完菜後順路上去市立圖書館逛時, 偶然看到一本股市理財書籍, 心中的財工之火又悄然燃起, 莫非又是新的心猿意馬輪迴開始了? 其實是太久沒玩網頁有點寂寞了, PHP, jQuery, MySQL, GAE, Python, ... 我又回來了! 但老實說才半年多沒碰, 還真有點生疏哩. 技術這東西還是需要常相左右溫故知新才能保持戰力. 年初處理完 Appfog 雲端服務後便沒再玩網頁與繼續開發財工軟體了, 改投入最愛的 Arduino 與務聯網學習, 雖然玩喜歡的物聯網很有趣, 但以前投入時間所締造的成果晾在那邊似乎有點可惜啊!

其實還有一個原因必須回來溫習一下網頁技術跟 Java, 那就是快到年底了, 去年押下的年度計畫也該開工了. 因為上頭之前有想要委外, 我也這麼期待著, 這樣省得我還要操煩資安問題. 自從資安當道之後, 我就不太願意自己做專案 (會有一大堆無聊的資安會議要開), 但觀望了快一年, 看來是沒經費委外了, 還是甘願一點捲起袖子自己做吧!

好在我勤於寫網誌做紀錄, 半年沒摸就生疏的東西在猛啃後一兩天內就迅速回魂了, 兩天就搞定了改版問題. 但完成後就煩惱要把程式放在哪家主機上跑為宜? 功能符合要求又穩定的免費主機坦白說一家也沒有, 天下沒有白吃的午餐, 還是找一家好一點的主機商付點錢取得運算資源算了.

過去幾年我用過好多免費主機, 到目前為止還在使用的有下列四家 :

1. Hostinger :

此英國主機商是我最滿意的一家, 不僅執行速度快, 而且管理介面很漂亮, 完全符合我的全部 cron 作業. 但是免費帳戶可用的 CPU 資源不能超過 70% 太多次, 否則會被停權.




升級為 Premium 用戶一次繳 48 個月, 每個月為歐元 1.99 元, 總價 95.52 歐元, 約台幣 3333 元, 平均每個月 70 元左右, 大概是一個午餐便當, 一年約 840 元, 似乎不算太貴.  

2. 2freehosting : 

這家主機最大的問題是跑 cURL 有問題, 連跑抓取台股盤網頁表頭的日期都抓不到, 但是執行 phpino.php 又顯示它有支援 cURL, 只是版本比較新, 或許這是原因. 它的  cPanel 介面與 Hostinger 很像, 不知是否為二房東.

3. 000webhost :

這家是我剛開始學 PHP 時使用的主機商, 介面雖然陽春, 但好處是不會因為長期未使用就砍你帳戶, 但缺點也是無法順利執行 cURL, 適合開發只用到資料庫或檔案管理的專案.

4. 1freehosting :

這家主機商是我使用最久的一家, 之前測試 PHP 的範例都放在這裡. 如果太久沒去拜訪或負載過重也是會被停權的. 以前被停過一次, 後來寫信去說明後就一直沒出過大問題, 僅偶而發生無法連線問題而已.

考慮過後決定選擇 Hostinger, 因為只有這家可以順利且快速地跑 cURL, 這是最重要的. 付費會員頻寬, 空間, 資料庫, 郵件與 FTP 帳號等均無限制, 另外它也提供每周資料備份與 30 天內不滿意退款, 以及進階 cron 作業, 也許可以不必用到 GAE 去觸發 cURL, 網站本身就可以辦到.

參考 :

#  'onDblClickRow' function on the datagrid
Configuring cron jobs with Windows
https://www.hostinger.co.uk/web-hosting


# Hostinger-新加坡低價無限虛擬主機申請介紹

2016年11月8日 星期二

20W 太陽能板安裝測試 (二)

昨天週日傍晚終於完成 20W 小型太陽能板安裝, 但已夕陽西下, 要準備晚餐, 等到吃過晚飯, 整理打掃妥當後才將這幾天測試好的程式趕緊修改, 測試完成已八點矣, 趕緊將蓄電池, 充電控制器, 以及Arduino 控制板接好線, 同時將樓上的小米路由器 (設定為 Repeater) 打開以延長 WiFi 涵蓋範圍, 測試結果正常. 前篇文章參考 :

# 20W 太陽能板安裝測試 (一)

在上一篇文章的測試 4 中, 傳送資料到 ThingSpeak 的指令是放在 loop() 迴圈中, 由於 ThingSpeak 有每次傳送間隔不可低於 15 秒限制, 使得偵測光度變化的光敏電阻或偵測物體移動的 PIR 模組會被 15 秒限制住而無法快速做出反應. 為了解決此問題, 我改為使用 TimeAlarm 函式庫以便設定計時器固定每 60 秒將 DHT 模組, PIR 模組, 以及光敏電阻所偵測到的溫溼度, 光度, 以及移動捕捉次數傳送到 ThingSpeak 記錄起來, 而需要即時反應變化的光敏電阻偵測與 PIR 移動偵測就放在 loop() 迴圈裡, 關於 TimeAlarm 參考 :

利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)

程式如下 :

測試 5  :

#include <SoftwareSerial.h>
#include <TimeAlarms.h>
#include "DHT.h"
#define DHTTYPE DHT11
#define DEBUG true

const byte pirPin=2; //PIR ouput connected to Arduino D2 (int 0)
const byte ledPin=13;
const byte relayPin=6;
const byte dhtPin=5;
const byte onLevel=20; //0(dark)~100(bright)
const byte offLevel=30; //0(dark)~100(bright)
const String ssid="EDIMAX-tony";
const String pwd="1234567890";
String api_key="NO5N8C7T2KINFCQE";  //Thingspeak API Write Key
int triggerCount=0; //interrupt counter
int triggerLimit=5; //cieling of trigger Counter

SoftwareSerial esp8266(7,8); //(RX,TX)
DHT dht(dhtPin, DHTTYPE); // Initialize DHT sensor

void(* resetFunc) (void)=0; //declare reset function at address 0

float c; //celsius
float f; //fahrenheit
float h; //humidity
int l; //luminance

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  /*sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address  */
  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  attachInterrupt(0, int0, RISING); //assign int0
  Alarm.timerRepeat(60, toThingSpeak);
  }

void loop() {
  c=dht.readTemperature();
  f=dht.readTemperature(true);
  h=dht.readHumidity();
  int cds=analogRead(A0); //get cds voltage (0~5V to 0~1023 dark)
  l=100-map(cds,0,1023,0,100); //map 0~1023(dark) to 0(dark)~100%
  Serial.print("C=");
  Serial.print(c);
  Serial.print(" ");
  Serial.print("F=");
  Serial.print(f);
  Serial.print(" ");
  Serial.print("H=");
  Serial.print(h);
  Serial.print(" ");  
  Serial.print("L=");
  Serial.print(l);
  Serial.print(" ");  
  Serial.print("T=");
  Serial.println(triggerCount);
  if (l < onLevel) {
    digitalWrite(relayPin, HIGH);  //turn on
    digitalWrite(ledPin, HIGH);
    }
  else if  (l > offLevel) {
    digitalWrite(relayPin, LOW);   //turn off
    digitalWrite(ledPin, LOW);
    }
  Alarm.delay(1000);
  }

void toThingSpeak() {
  String data="GET /update?api_key=" + api_key + "&field1=" + (String)c +
              "&field2=" + (String)f + "&field3=" + (String)h + "&field4=" +
              (String)l + "&field5=" + (String)triggerCount + "\r\n";
  String cmd="AT+CIPSTART=\"TCP\",\"184.106.153.149\",80\r\n";
  sendData(cmd, 1000, DEBUG);
  cmd="AT+CIPSEND=" + (String)data.length() + "\r\n";
  sendData(cmd,1000,DEBUG);
  String res=sendData(data, 2000, DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  //Serial.println("===" + res + "===");
  if (res.indexOf("Unlink") == -1) { //if no auto unlink
    sendData("AT+CIPCLOSE\r\n",2000,DEBUG); //close session
    }
  if (res.indexOf("busy") != -1 || res.indexOf("ERROR") != -1) {
    //Serial.println("*****busy/ERROR*****");
    resetFunc();
    }
  //Serial.println(res.indexOf("busy"));
  //Serial.println(res.indexOf("ERROR"));
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),10000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

void int0() { //interrupt handler
  if (triggerCount <= triggerLimit) {++triggerCount;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

這裡要傳送的參數如溫溼度, 光度, 以及移動次數因為傳送給 ThingSpeak 的函數要擷取的關係, 在此都設為全域變數. PIR 輸出接 Arduino 的 D2 (int 0), 當偵測到移動時才觸發執行 int0() 函數, 而傳送資料到 ThingSpeak 則由 TimeAlarm 函式庫的 Alarm.timerRepeat() 函數每 60 秒觸發執行一次 (計時器中斷), 這樣便不會被 ThingSpeak 的 15 秒限制拖住了.

我看程式有順利執行資料傳送後才關掉電腦返回高雄. 今天觀察 ThingSpeak 所記錄的資料一切都正常, 發現一天之中氣溫從早上的 25 度逐漸上升, 濕度則反向逐步下降, 到中午 14:30~:15:30 左右到達最高溫 31 反轉向下, 濕度則從午後低點的 36% 回升, 如下圖所示 :


這裡的 TriggerCount 就是 PIR 感測器所偵測到的移動次數累計值, 從時間判斷, 那兩個突波應該是爸早上要出去時觸發的. 到這裡這次的實驗就告一個段落了, 接下來有時間要製作一個木製的機箱來放電池, 充控, 以及 Arduino+ESP8266 IOT 模組與感測器了. 或者, 也可以將上面程式改為使用 Blynk 以便在手機 App 上觀看結果. 

另外, 還有一個感測數據尚待紀錄, 那就是蓄電池的電壓! 這可能需要用一個電阻分壓器接到 Arduino 來換算. 如果以 15V 當最高電壓, 轉換成 5V 就需要除以 3, 這可以用 2K+1K 或 20K+10K 分壓器來做, 10K 壓降接到 Arduino 的 A1, 經內部 A/D 轉換成 0~1023 數值, 再反推為 0~15V 即可, 當然這需要取平均數並且經過校正才行.