2013年6月27日 星期四

利用 GAE 與 Gmail 傳送電子郵件

由於 Appfog 似乎無法傳送信件 (老實說是我還不清楚), 而一般的免費 PHP 主機如果傳送太多郵件可能會被 suspend 帳號 (例如累積送出 5000 封信), 只有 GAE 不會搞這樣的不明不白停止人家帳號的小伎倆. 它的收費政策寫得很清楚, 祇要不超過免費界線, 定時寄送郵件的功能非常穩定. 以下說明如何利用 GAE 寄信, 細節請參考 :
GAE 是利用 mail 模組來寄信的, 因此使用前必須匯入 mail :

from google.appengine.api import mail

GAE 的 mail API 提供兩種寄送 E-mail 郵件的方式 :

  1. 直接呼叫 mail.send_mail() 方法 :
    這是最簡單的方式, 只要有寄件者, 收件者, 主旨與內容即可寄信, 當不需要用到副本 cc, 密件副本 bcc , 與附件時適用 :

    sender="abc123@gmail.com"
    subject="mail test from abc123@gmail.com at GAE"
    to="xyz@yahoo.com"
    body="Hello! World!"
    mail.send_mail(sender, to, subject, body)

    如果來自網頁以 POST 方式傳送, 可用例如 self.request.get("subject") 來取得. 由於主旨 subject 與內容 body 可能含有中文, 所以須加上 encode("utf-8") 以免亂碼 :

    body=self.request.get("content").encode("utf-8")
  2. 使用 mail.EmailMessage 物件 :
    此物件可指定副本 (Cc) 與密件副本 (Bcc) 收件人, 也可以傳送附件檔案. 注意, 收件人 to 與 cc, bcc 可以寄給不只一個對象, 只要用逗號或分號隔開即可 :

    msg=mail.EmailMessage()
    msg.sender="abc123@gmail.com"
    msg.subject="mail test from abc123@gmail.com at GAE"
    msg.to="xyz@yahoo.com, tuv@gmail.com"
    msg.cc="abc@yahoo.com, kkk@gmail.com"
    msg.bcc="nono@yahoo.com, ppp@gmail.com"
    msg.html="<b>Hello! World!</b>"

2013年6月22日 星期六

AmyPHP 更新使用者的 BUG

今天又發現了 AmyPHP 一個嚴重 BUG, 中午吃完香 Q 麵回來, 用 iPad 上 stockbot 網站看看執行狀況, 卡好, superuser 竟然無法登入! 這是怎麼回事? 回想了一下出去吃飯前我改了 superuser 的資料, 該不會是 update user 時把密碼也改了吧? 果然, 檢查 sys.php 的 update_users 模組, 嘿嘿, 當初把 add_users 的程式複製過來改時, 忘記把 md5 拿掉, 所以原本的 md5 再 md5 一次存進去, 當然密碼也被改啦! 正確作法應該先檢查使用者是否有更改密碼, 沒改就原來的 md5 密碼存進去, 有改就必須 md5 加密 :

              $id=$_REQUEST["id"];
              //先檢查密碼是否有更改
              $RS=search("users","id",$id);
              if (is_array($RS)) { //有找到
                  if ($_REQUEST["password"]==$RS[0]["password"]) { //沒改
                      $data_array["password"]=$_REQUEST["password"];
                      } //end of if
                  else { //有改
                      $data_array["password"]=md5($_REQUEST["password"]);
                      } //end of else
                  } //end of if

AmyPHP 微框架修改安裝程式列表

今天發現 AmyPHP 管理頁面的應用程式列表向右撐破了 jQuery UI tabs 的框框, 非常的不協調, 這在一般表格也會有此現象, 例如使用 Javascript 的 join() 方法將陣列 tabs 組合成陣列時, 預設是用逗號 "," 串接各元素 :

var table_names=tabs.join(); //預設以 "," 串接
alert table_names;

這樣會得到類似下列這樣毫無空隙的字串 (沒有空格) :

snowball,z0000,z0050,international,inventory,tracking,report,compound_annuity,cron,stock_settings

這個字串如果放在表格中, 就會向右撐破表格的寬度, 破壞整個頁面的規劃. 如果這字串的逗號後面有一個空格, 那就不一樣了 :

snowball, z0000, z0050, international, inventory, tracking, report, compound_annuity, cron, stock_settings

這個字串會被表格的 td 寬度限制, 到右邊界時會折到下一行顯示, 因此不會破壞頁面. 所以 join 時要用 ", " :

var table_names=tabs.join(", ");  //逗號後面加一個空格

同樣的, 在 PHP 用 join() 串接陣列元素時, 也要用這樣的方式為宜 :

$table_name=join(", ", $tabs); //逗號後面加一個空格

而 AmyPHP 需要修改的地方有兩個, 一是 apps/app_install.php 結尾處, 把 tab_names 與 table_names 之串接多加一個空格 :

/*--- 以下是系統作業, 不可修改刪除 ---*/
//更新系統 apps 資料表的 app_name='app' 欄位
$data_array["installed"]="Y";                            //已安裝 "Y"/"N"
$data_array["show_tabs"]="Y";                        //顯示頁籤 "Y"/"N"
$data_array["tab_names"]=join(", ",$tabs);      //空格是為了避免撐破表格
$data_array["table_names"]=join(", ",$tables); //空格是為了避免撐破表格
$data_array["remark"]="OK";                           //安裝結果

二是 sys.php 的 case "remove_app" 模組, 要刪除應用程式 (解除安裝) 時, 須將該應用程式所安裝的 tab 移除, 也要將相關資料表移除, 這時就要從 apps 資料表中讀出這些資料表與頁籤名稱字串, 用 explode 分拆時也要配合以逗號與空格為界 :

//分隔字元為 ", ", 其中空格是為了避免顯示時撐破表格
$tab_names=explode(", ",$RS[0]["tab_names"]); //空格
$table_names=explode(", ",$RS[0]["table_names"]);

判斷 MySQL 是否找到紀錄的方法

在 mysql.php 函式庫中, 可以用 search() 或 run_sql() 函式去資料表撈資料, 例如 :

$SQL="SELECT * FROM `report` WHERE `date_time` LIKE '".$today."%'";
$RS=run_sql($SQL);

或者 :

$RS=search("apps","app_name",$app_name);

這些都會呼叫 PHP 原生的 mysql_query() 函式執行 SQL 指令, 然後以 mysql_numrows()  函式取得資料筆數, 作為迴圈的終值, 在每個迴圈內呼叫 mysql_fetch_array() 取得每筆資料的一維關聯式陣列 (用欄位名稱當索引), 再把此陣列存入一個索引陣列中.

         $result=mysql_query($SQL, $conn); //執行 SQL 指令
         for ($i=0; $i<mysql_numrows($result); $i++) {
               $RS[$i]=mysql_fetch_array($result);
               } //end of for
         if (mysql_error($conn)) {return $result;}
         else {return $RS;}

所以如果有找到一筆紀錄, 則最後傳回來的是一個二維陣列, 例如第一筆的 id 欄位為 $RS[0]["id"]. 如果沒找到任何紀錄的話, 預設傳回 FALSE (即 mysql_query 的傳回值), 除錯模式則會傳回錯誤原因字串, 例如 "MySQL SELECT Error : ......".
所以, 要如何判斷有沒有撈到資料呢? 有兩個辦法 :
  1. 用 is_array() :
    if (is_array($RS)) {....}
    else {....}
  2. 用 sizeof() :
    if (sizeof($RS)!=0) {...}
    else {...}
如果有找到紀錄, 則傳回值 $RS 必定是二維陣列, is_array() 就傳回 TRUE, 而 sizeof() 傳回撈到的資料筆數 (所以要計算筆數除了用 count($RS) 外, 也可以用 sizeof($RS)).

2013年6月21日 星期五

PHP 從日期字串求星期幾的方法

從資料庫中的日期時間欄位 (格式是 YYYY-MM-DD HH:mm:SS) 取得資料後, 想要從日期計算當天是星期幾, 這樣顯示在網頁中會比較 user-friendly, 作法是利用 date() 函式的 "w" 參數與該日任意時間之時戳 (time stamp), 詳細如下 :

$week=Array("日","一","二","三","四","五","六");
$date_time="2013-06-21 15:05:24";
list($date)=explode(" ", $date_time); //取出日期部份
list($Y,$M,$D)=explode("-",$date); //分離出年月日以便製作時戳
echo $date."  (星期".$week[date("w", mktime(0,0,0,$M,$D,$Y))].")";

這樣便會顯示 2013-06-21 (星期五) 了.

jQuery 的 val() 設值問題 (setter)

今天在用 jQuery 的 val() 設定 test_date 這個  text input 元素的值時, 發現此函數會計值, 如果要設定文字進去, 務必左右加上引號, 例如 test_date 是一個日期欄位, 所以從資料庫讀出來時是 2013-06-21 這樣的值, 原先我是這樣設值的 :

$("#test_date").val(2013-06-21);

結果在文字欄位中顯示的是 1986, 因為 val() 把 2013-06-21 看成是兩次減法, 所以 2013 減掉 27 後得 1986, 正確用法是用字串 :

$("#test_date").val("2013-06-21");

專案中的實際例子是從資料庫中讀出 test_date 欄位 :

$test_date=$RS[0]["test_date"];

然後在 heredoc 中把此值放入 val() 中 :

$("#test_date").val("{$test_date}"); //設定初始值 (正確方法)

原先錯誤的方式是 :

$("#test_date").val({$test_date}); //設定初始值 (錯誤方法)

我搞清楚為什麼會是 1986 後, 原先以為只要把 test_date 轉成字串就可以, 於是參考 Jollen's PHP 的 "如何做 PHP 的型別轉換?", 用 (string) 去把 $test_date 轉成字串, 但發現這是沒用的 :

$test_date=(string)$RS[0]["test_date"];

$("#test_date").val({$test_date}); //設定初始值 (錯誤方法)

其實只要像上面說的那樣, 左右用引號括起來就可以了.

更新 jqueryui.php 函式庫的 get_datepicker 函式

為 stock_settings 添加 test_date 欄位時發現 get_datepicker() 函式中的 input 元素忘了給 name 屬性, 這樣表單傳送時將無法傳出 test_date 變數, 故加以修改如下 :
function get_datepicker($id, $change_YM=false, $show_week=false, $locale="zh-TW") {
  .....
  $opt="{".join(",", $opt)."}";
  $result="<input id='".$id."' name='".$id."'>\n".
              "<script>$('#".$id."').datepicker(".$opt.");\n</script>\n";
  return $result;
  }

create_report.php 的測試模式

昨天對資料庫做了一項重大變更, stock_settings 資料表加入 test_mode 欄位, 這樣要做測試時只要線上修改, 不需要改程式 :

//讀取 stock_settings (測試用)
$RS=search("stock_settings");
$test_mode=$RS[0]["test_mode"];   //"Y"=測試模式
$test_date=$RS[0]["test_date"];       //"Y"=測試日期
//製作今日日期
$today=date("Y-m-d"); //2011-12-10
if ($test_mode=="Y") { //測試模式時
    $YMD=explode("-", $test_date);
    $test_date=mktime(0,0,0,$YMD[1],$YMD[2],$YMD[0]); //時分秒月日年
    $today=date("Y-m-d",$test_date); //覆蓋 $today, 擷取測試日期之報告
    } //end of if
//讀取 report 資料表, 判斷本日是否已有報告
$SQL="SELECT * FROM `report` WHERE `date_time` LIKE '".$today."%'";
$RS=run_sql($SQL);
if ($test_mode=="Y") {$RS="";} //測試模式時以字串覆蓋陣列,允許重複產生報告
if (is_array($RS)) {echo "本日已有報告";} //工作模式,不允許重複產生報告
else { //本日未有報告, 進行擷取
        ...
       }

2013-06-22 註 :
後來在之前寫的程式發現用 list 更簡潔 :

if ($test_mode=="Y") { //測試模式時
    list($Y,$M,$D)=explode("-", $test_date); //分出年月日
    $test_date=mktime(0,0,0,$M,$D,$Y); //時分秒月日年
    $today=date("Y-m-d",$test_date); //覆蓋 $today, 擷取測試日期之報告
    } //end of if

2013年6月20日 星期四

網頁資料擷取易容術

以網頁程式定期抓取網站資料時, 最忌諱被誤認為是駭客攻擊, 導致被拒絕存取, 而 PHP/cURL 可以使用易容術來避免誤會, 方法是在 cURL 的選項設定 setopt 中, 把 USERAGENT 設為一般常見瀏覽器, 例如以 Chrome 而言 :

$UA="Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36(KHTML, like Gecko) ".
        "Chrome/27.0.1453.110 Safari/537.36";

這樣在 PHP 擷取程式中, 以這個 $UA 作為 cURL 之 CURLOPT_USERAGENT 值, 就可以讓程式看起來比較像是用一般瀏覽器在下載檔案了 :

curl_setopt($ch, CURLOPT_USERAGENT, $UA);

當然, 光是這樣並沒有辦法完全避免誤會你是用程式抓檔案, 固定的來源 IP 與下載週期也很容易被識破. 當然, 來源 IP 是沒辦法改的, 而用 cron table 觸發也會導致固定的下載頻率. 不過這個可以用隨機的 sleep() 來修飾, 亦即在進行擷取前, 先隨機休眠一段時間  :

sleep(rand(5, 50));  //休息 5~50 秒再進行擷取

這樣在伺服端看來就比較不那麼突兀了.

2013年6月19日 星期三

Appfog 的 PHP 以 ini_set() 設定時區無效問題

今天下午把 stockbot 專案部署到 Appfog, 晚上檢查 cron log 發現時間都不對, 差了 8 個小時, 顯然還是主機所在位置 (愛爾蘭) 的時間. 我是用這個指令來指定為台北時間 :

ini_set('date.timezone','ASIA/Taipei')

這在別的 PHP Hosting 似乎沒問題, 但到 Appfog 就有問題, 可見 Appfog 無法更改 php.ini 內容. 還是用 date_default_timezone_set() 比較保險. 所以我把每一個 cron 程式頭的 ini_set 都改成如下 :

#設定時區以使 cron_log 紀錄台灣時間
@ini_set('date.timezone','ASIA/Taipei');  //不一定有效
@ini_set("max_execution_time","120");     //不一定有效
date_default_timezone_set("Asia/Taipei");
echo date_default_timezone_get()."<br>";

重新發佈之後, cron log 資料表所記錄的就是台灣時間了.
另外, 同時也發現一個問題, 也就是若 stock_list 資料表是空的 (也就是還沒執行過 fetch_twse_daily_close.php), 那麼去抓 Yahoo 資料的 cron 程式也會做白工, 因為每一次從 stock_list 抓幾筆 stock_id, 便去擷取相應網頁來分析, 如果 stock_list 是空的, 那麼迴圈也就不會跑. 所以, 相關的 2 個 cron 程式要做些修改 :

$SQL="SELECT `stock_id` FROM `stocks_list` WHERE NOT `stock_id` LIKE '0%'";
$RS=run_sql($SQL);
if (count($RS)==0) {
    header('Location: fetch_twse_daily_close.php');
    } //end of if

這樣一來, 只要是交易日, 只有第一個 cron 程式會失敗, 但它會觸發 twse 的擷取動作, 填滿 stock_list 資料表, 第二個執行的 cron 程式就會成功了. 但若系統啟動時為非交易日, 則要到下一個交易日收盤資料可以抓到時才會真正抓到資料, 在此之前全部都是做白工.