聚會時間公告: 11月份聚會為11月15號星期六下午兩點在MocaMona / 講者:shawn Topic: Hello BIOS: EFI, an opensource firmware framework

十一月 10, 2008
» Ruby on Rails 初學入門(4) - Scaffold 的程式說明(MVC)

瞭解Scaffold 的程式流程是怎麼跑後, 再來就由它的程式碼來學習囉~

先看下述幾個頁面:

  • Controller: app/controllers/posts_controller.rb
  • Model: app/models/post.rb
  • View: app/views/posts/edit.html.erb # 編輯
  • View Layout: app/views/layouts/posts.html.erb # 此檔案是 posts html layout 的 template 檔.

下述的內容可搭配此圖一起看:

RoR Scaffold 程式說明

Index Controller 流程

  1. 進入 Controller index function
  2. Post find 去將 Model 的 Post class 產生實體(new)回傳
  3. 決定要秀 HTML 頁面 或 XML 頁面
  4. View / Layout
進入 Controller index function

程式進入點是 /posts 或 /posts.xml 預設就是會跑 Controller index function.

先看 Controller: app/controllers/posts_controller.rb 的 index function.

class PostsController < ApplicationController
  # GET /posts
  # GET /posts.xml
  def index
    @posts = Post.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @posts }
    end
  end
end

由此可知 Controller 是繼承自 ApplicationController(app/controllers/application.rb), 而註解寫到的就是存取此 index 可以用 GET /posts 和 GET /posts.xml 兩種方式, 來分別看到 View 的 Template 結果和 XML 結果.

def index

def index 是 function, 到底是對應到哪個頁面, 可由下述步驟查詢:

bash$ rake routes 可以看到:

  • posts GET  /posts  {:action=>"index", :controller=>"posts"}
  • formatted_posts GET   /posts.:format  {:action=>"index", :controller=>"posts"}

此處的 def index 就是 routes 的 :action => "index"

Post find 去將 Model 的 Post class 產生實體(new)回傳
@posts = Post.find(:all)

到此處時, 這邊就是使用到 Model 的 Post class, 這段程式的流程會從 Post.find(:all) 直接進入 Model, 完成後再繼續下面的處理.

於 Post.find(:all) 可見: Model: app/models/post.rb, 程式碼只有下面2行:

class Post < ActiveRecord::Base
end

find() 這是於 ActiveRecord 的 function, 因為 class Post 是繼承自 ActiveRecord, 所以可直接使用, 也是於此做其它資料正確性的檢查.

決定要秀 HTML 頁面 或 XML 頁面
respond_to do |format|

再來看最後輸出, 要輸出前為何還要加 respond_to? 且同一個 function 是如何輸出兩種不同的結果?

於程式可以看到 respond_to 裡面有包兩個輸出檔: format.html / format.xml, 由此可知是由此產生 HTML/XML 檔輸出.

respond_to 判斷何時要送 HTML, 何時要送 XML, 主要是靠 "HTTP 的 Accept-Type", 若不用 respond_to 的話, 程式得要改寫成下述:(參考: REST on Rails指南5: respond_to)

def index(client_format)
    @posts = Post.find(:all)

    if client_format == "text/html"
        # TO DO: render the default template
    elsif client_format == "application/javascript"
        # TO DO: return some javascript
    elsif client_format == "application/xml" || client_format == "text/xml"
        # TO DO: return some XML back the client
        # ... more elsif statements here for each MIME type you want to support
    end
end

因為 respond_to 處理掉上述的事情, 所以就可以用下述存取看看, 就可以得到不同的結果:

  • http://DOMAIN/posts/1
  • http://DOMAIN/posts/1.xml
View / Layout
format.html # index.html.erb

於 format.html 這邊, 它就會去讀取 View 的 index.html.erb 檔, 而 View 裡面也有固定的 Layout file 做定義.

先看 Layout file: (只看 body 部份)

<body>
    <p style="color: green"><%= flash[:notice] %></p>
    <%= yield  %>
</body>
<%= flash[:notice] %>

此段變數的訊息會從 Controller 取出, 於 Create / Update 的 Controller 完成後, 就會看到 Post was successfully created. 等訊息.

<%= yield  %>

而此行, 就會把 View 的內容放進去(取代 yield), 例如 View 的編輯頁面:

View: app/views/posts/edit.html.erb, 內容大致如下:

<%= error_messages_for :post %>
<% form_for(@post) do |f| %>
  <p>
    <b>Id</b><br />
    <%= f.text_field :id %>
  </p>
<%= error_messages_for :post %>

此行就是 Model 有錯誤時, 錯誤訊息(:message)會於此印出. ex: 若於 Model 加入此段: validates_length_of :title, :minimum => 20, :message => "最少要 %d 字元", 那 title 若沒有輸入超過 20個字, 就會顯示錯誤訊息(若有任何錯誤, 資料就不會被新增/修改).

十一月 9, 2008
» Ruby on Rails 初學入門(3) - Scaffold 的 CRUD 程式流程

要感受 Ruby on Rails 的快速, 由 Scaffold 來學習/修改會是比較快的方法, 所以先看 Scaffold 產生的程式流程是怎麼跑的.

RoR REST 與 HTTP 標準協定對應

首先要先了解 RoR controller 是REST實作:(下面是 CRUD 與 REST 的對照)

  • CREATE = POST: 新增資料
  • READ = GET: 取得資料
  • UPDATE = PUT: 更新資料
  • DELETE = DELETE: 刪除資料

Scaffold Controller Function 列表

Scaffold 產生於 Controller 的 function 有 7 個: (routes => map.resources :posts)

  • show: 處理單一資料的 GET request, ex: http://domain/posts/1, routes: GET /posts/:id
  • create: 處理 POST request, 並將建立新的一筆資料, ex: http://domain/posts, routes: POST /posts
  • update: 處理 PUT request, 並將指定的資料做更新, ex: http://domain/posts/1, routes: PUT /posts/:id
  • destroy: 處理 DELETE request, 清除此筆資料紀錄, ex: http://domain/posts/1, routes: DELETE /posts/:id
  • index: 秀出目前所有資料, http://domain/, routes: GET /
  • new: 建立新資料用的欄位頁面, 送出會執行 create 功能, ex: http://domain/posts/new, routes: GET /posts/new
  • edit: 取得並列出指定資料以供修改用, 送出會執行 update 功能, ex: http://domain/posts/1/edit, routes: GET /posts/:id/edit
  • 註: Controller 每個 function 都等於是一個頁面.

Scaffold 產生 CRUD 的運作流程

通常網頁程式的 Form 的寫法是 透過 GET/POST 將資料送到 處理/檢查頁面, 若有錯誤直接把錯誤訊息秀出, 再將原先 Form 的內容頁 load 出來/或者導回原本秀 Form 的頁面, 讓 User 再次輸入資料, 新增/修改 完成後, 再導到完成頁.

在此把 Scaffold 產生出來頁面的處理流程: (都由首頁出發)

  • Index -> New -> Create(Action) -> Index
  • Index -> Show -> Edit -> Update(Action) -> Index
  • Index -> Edit -> Update(Action) -> Index
  • Index -> Destroy(Action) -> Index
  • 註: 標 Action 的, 都沒有 View 的 HTML 頁面, 都是透過這幾個 Controller 的 function 來處理資料(處理資料於檢查/寫入 DB 等是 Model 負責), 這些 Action 都沒有頁面, 都是處理完成就導到完成頁, 處理/檢查失敗則會把原本上一個產生 Form 的頁面 load 進來(網址會是 Action 的頁面網址), 可參考下圖.
RoR Scaffold 頁面 CRUD 處理流程

在此附上此圖的 dia 原始檔(之後所有圖檔都會擺在這個檔案裡面.), 若覺得這樣子 畫不對 或者 圖/線 用的不好, 願意幫我改的話, 請自行下載修改, 再讓我更新圖片. 謝謝.

關於 Rails 的架構, 可另外參見此圖: (取自: Ruby on Rails (ROR))

Rails 架構圖

十一月 6, 2008
» Ruby on Rails 初學入門(2) - config 與 MVC 關連性

Ruby on Rails 剛建立完專案後, 馬上要碰到的就是 config/* 和 MVC 的程式撰寫/修改等, 所以此篇就先簡單介紹 config/* 與 MVC 之間的檔案關聯性.

config 目錄的檔案列表

config/
  environments/ - 目錄
  initializers/ - 目錄
  boot.rb
  database.yml
  environment.rb
  routes.rb

MVC 的定義

RoR 的程式主要就是架構在 MVC (Model / View / Controller) 來撰寫, 那 MVC 代表哪些東西?

  • Model: 負責處理與資料庫之間的溝通 (繼承自 ActiveRecord::Base 的 class)
  • View: 產生前端頁面的 Template, 使用者是透過 View 秀出的頁面去送出 GET/POST 等要求.
  • Controller: 處理使用者的要求(POST/Create、GET/Read、PUT/Update、DELETE/Delete), 若要存取資料庫的資料, 則與 Model 溝通(new/find Model class), 最後再將資料送給 View, 透過 View 的 Template 產生使用者看到的頁面.
  • 更多 MVC 介紹可見: MVC - 維基百科,自由的百科全書

Rails 的 config 到 MVC 流程說明

首先若使用者輸入 http://DOMAIN/posts/1, 當此網址丟到 Server 後, Rails 那會做哪些事情? (註: 下述程式流程採用 scaffold 預設流程)

Rails user input data to server

Ruby on Rails request and MVC process

上圖中, 畫的就是使用者輸入的資料送進 Rails 後, Rails 程式運作流程:

  1. 資料由 Server 接收送到 Rails framework 中.
  2. 首先進入 config/boot.rb, boot.rb 會去呼叫 environment.rb.
  3. environment.rb 會將 environments, initializers 這兩個目錄下的檔案都載入進來, 最重要的 database 設定檔也是在此同時被載入進來(註: 這些載入我並沒有詳看 Source code, 是採猜測的方式, 若有錯誤還請指點), 於 environment.rb 會去拉 app 目錄下的 overrides.rb (此檔案預設不存在, 是可以讓你做額外擴充用, ex: 錯誤訊息中文化, 就是可用此將檔案, 將物件取代掉).
  4. 最後進入 routes.rb, 此檔是設定網址參數 如何拆解 並帶入 params 變數, 拆解完的變數再送給 app 的 Controller.
  5. 再來進入 app 的目錄, 就是跑 MVC 的程式.
  6. routes.rb 拆解的參數, 依上面的範例, 會是對應到 post GET /posts/:id  {:action=>"show", :controller=>"posts"}
  7. 拆解送到 Controller 會是 :controller => "posts", :action => "show", :id => "1", 於此會使用 posts controller 的 show action
  8. 於 Controller show 中, 使用 Post.find(params[:id]), 此 Post.find() 會去 Model 的 Post class 做 find() 動作.
  9. Controller 接收 Model 查 DB 後回傳的資料, 再將資料送到 View 的 Template, 於此就是回傳給使用者的頁面就產生完成.
  10. 再將此頁面回傳給使用者的瀏覽器即可.

在此附上上述兩張圖的 Dia 原始檔 (之後所有圖檔都會擺在這個檔案裡面.), 若覺得 畫的不對 或者 圖/線 用的不好, 表達不清楚等, 願意幫我改的話, 請自行下載修改, 再讓我更新圖片. Orz..

十一月 5, 2008
» Ruby on Rails 初學入門(1) - 快速建立Blog (CRUD)

大家對 RoR 的印象應該是開發很快, 可以很快的把雛型建立出來.

但是 Rails 的改版變動都非常大, 之前 10分鐘建立 Blog 的影片 和 目前市面上的書都是 Rails 1.x 版, 現在都是使用 2.x 版, 照著書上做的話, 會有一堆錯誤(程式都不會動), 於是來寫幾篇紀錄摸索的過程.

在此假設您已經建好基本環境, 並且知道 Ruby 的語法, 若不清楚, 可以參考下述兩篇:

快速建立 Blog 步驟

  1. 建立 Database blog
  2. 建立 Table posts (必須要用複數)
  3. 建立 Rails project
  4. 使用 scaffold 快速產生 CRUD
  5. 修改 routes 將首頁指向 posts/index

建立 Database / Table

照理說應該要用 db:migrate, 邊寫邊把 Table 欄位等建立出來, 不過剛開始還是先照傳統方法來做比較容易懂.

CREATE DATABASE blog;
use blog;
CREATE TABLE `posts` (
   `id` int(11) NOT NULL auto_increment,
   `title` varchar(200),
   `content` text,
   `created_at` datetime default NULL,
   `updated_at` datetime default NULL,
   PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

註:

  • id 是必須的(Rails 規定 Table 都需要建立此欄位)
  • created_at: Rails 會自動幫此欄位填入建立日期
  • created_at: Rails 會自動幫此欄位填入修改日期

建立 Rails Project

假設是要建立於 /var/www/blog, 步驟如下:

  1. cd /var/www
  2. rails blog # 這樣子就會於 /var/www/blog 產生 rails 所需檔案/結構
  3. rm public/index.html # 刪除預設首頁, 不刪掉就不會其它程式
  4. vim config/database.yml
    將 database: blog_development 修改成 database: blog

使用 scaffold 快速產生 CRUD

使用 scaffold 快速產生 CRUD 程式, 步驟如下:

  1. cd /var/www/blog
  2. script/generate scaffold post id:integer title:string content:text created_at:datetime # 把頁面想要秀/修改的資訊都列出來, Table 名稱於此處是用單數 (若前面沒建 DB 也可以跑這步驟, 查看修改 db/migrate/001_create_posts.rb, 再 rake db:migrate 也會建立 DB.)

scaffold 會建立下述檔案/資料:

Controller
  • app/controllers/posts_controller.rb
Model
  • app/models/post.rb
View
  • app/views/posts # 目錄
  • app/views/posts/index.html.erb
  • app/views/posts/show.html.erb
  • app/views/posts/new.html.erb
  • app/views/posts/edit.html.erb
  • app/views/layouts/posts.html.erb
View Style
  • public/stylesheets/scaffold.css
Helper
  • app/helpers/posts_helper.rb
Test
  • test/unit/post_test.rb
  • test/fixtures/posts.yml
  • test/functional/posts_controller_test.rb
Database migrate
  • db/migrate/001_create_posts.rb
config/routes.rb
  • map.resources :posts # 檔案上方會加入此行
修改 routes 將首頁指向 posts/index
  • vim config/routes.rb # 加入下述內容
    map.root :controller => "posts"

到此就可以於 http://DOMAIN_NAME/ 或 http://DOMAIN_NAME/posts/ 執行 新增/刪除/修改/查詢(CRUD) 的動作, 簡易的 Blog 就算完成囉~


查看有哪些 routes 規則

  1. cd /var/www/blog
  2. rake routes # 會列出現在的 routes 的規則有哪些

Scaffold 產生的程式內容

在此將 Controller / Model / View scaffold 的檔案內容先列出來. (在初學時, 常會需要參考 scaffold 怎麼寫的, 列出來參考也比較容易懂)

Controller
  • app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # GET /posts
  # GET /posts.xml
  def index
    @posts = Post.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @posts }
    end
  end

  # GET /posts/1
  # GET /posts/1.xml
  def show
    @post = Post.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @post }
    end
  end

  # GET /posts/new
  # GET /posts/new.xml
  def new
    @post = Post.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @post }
    end
  end

  # GET /posts/1/edit
  def edit
    @post = Post.find(params[:id])
  end

  # POST /posts
  # POST /posts.xml
  def create
    @post = Post.new(params[:post])

    respond_to do |format|
      if @post.save
        flash[:notice] = 'Post was successfully created.'
        format.html { redirect_to(@post) }
        format.xml  { render :xml => @post, :status => :created, :location => @post }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @post.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /posts/1
  # PUT /posts/1.xml
  def update
    @post = Post.find(params[:id])

    respond_to do |format|
      if @post.update_attributes(params[:post])
        flash[:notice] = 'Post was successfully updated.'
        format.html { redirect_to(@post) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @post.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /posts/1
  # DELETE /posts/1.xml
  def destroy
    @post = Post.find(params[:id])
    @post.destroy

    respond_to do |format|
      format.html { redirect_to(posts_url) }
      format.xml  { head :ok }
    end
  end
end
Model
  • app/models/post.rb
class Post < ActiveRecord::Base
end
View
  • app/views/posts/index.html.erb # 全部列表
    <h1>Listing posts</h1>

    <table>
      <tr>
        <th>Id</th>
        <th>Title</th>
        <th>Content</th>
        <th>Created at</th>
      </tr>

    <% for post in @posts %>
      <tr>
        <td><%=h post.id %></td>
        <td><%=h post.title %></td>
        <td><%=h post.content %></td>
        <td><%=h post.created_at %></td>
        <td><%= link_to 'Show', post %></td>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
      </tr>
    <% end %>
    </table>

    <br />

    <%= link_to 'New post', new_post_path %>
  • app/views/posts/show.html.erb # 查詢某筆資料
    <p>
      <b>Id:</b>
      <%=h @post.id %>
    </p>

    <p>
      <b>Title:</b>
      <%=h @post.title %>
    </p>

    <p>
      <b>Content:</b>
      <%=h @post.content %>
    </p>

    <p>
      <b>Created at:</b>
      <%=h @post.created_at %>
    </p>


    <%= link_to 'Edit', edit_post_path(@post) %> |
    <%= link_to 'Back', posts_path %>
  • app/views/posts/new.html.erb # 新增
    <h1>New post</h1>

    <%= error_messages_for :post %>

    <% form_for(@post) do |f| %>
      <p>
        <b>Id</b><br />
        <%= f.text_field :id %>
      </p>

      <p>
        <b>Title</b><br />
        <%= f.text_field :title %>
      </p>

      <p>
        <b>Content</b><br />
        <%= f.text_area :content %>
      </p>

      <p>
        <b>Created at</b><br />
        <%= f.datetime_select :created_at %>
      </p>

      <p>
        <%= f.submit "Create" %>
      </p>
    <% end %>

    <%= link_to 'Back', posts_path %>
  • app/views/posts/edit.html.erb # 編輯
    <h1>Editing post</h1>

    <%= error_messages_for :post %>

    <% form_for(@post) do |f| %>
      <p>
        <b>Id</b><br />
        <%= f.text_field :id %>
      </p>

      <p>
        <b>Title</b><br />
        <%= f.text_field :title %>
      </p>

      <p>
        <b>Content</b><br />
        <%= f.text_area :content %>
      </p>

      <p>
        <b>Created at</b><br />
        <%= f.datetime_select :created_at %>
      </p>

      <p>
        <%= f.submit "Update" %>
      </p>
    <% end %>

    <%= link_to 'Show', @post %> |
    <%= link_to 'Back', posts_path %>
  • app/views/layouts/posts.html.erb # posts html layout 的 template 檔.
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
      <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
      <title>Posts: <%= controller.action_name %></title>
      <%= stylesheet_link_tag 'scaffold' %>
    </head>
    <body>

    <p style="color: green"><%= flash[:notice] %></p>

    <%= yield  %>

    </body>
    </html>

十月 8, 2008
» PHP 設定 session 的過期時間

現在的網站都流行登入後, 除非按了登出, 不然就永遠是登入狀態(永不過期).(註: 相對需要更注意使用者帳號的安全問題)

不過, 這樣子設的話, 計算 目前站上人數 就得要另外想辦法做囉~ :)

Session 過期時間程式

下述程式轉載自: Cross-Browser Session Starter

<?php
function start_session($expire = 0)
{
    if ($expire == 0) {
        $expire = ini_get('session.gc_maxlifetime');
    } else {
        ini_set('session.gc_maxlifetime', $expire);
     }

    if (empty($_COOKIE['PHPSESSID'])) {
        session_set_cookie_params($expire);
        session_start();
    } else {
        session_start();
        setcookie('PHPSESSID', session_id(), time() + $expire);
     }
}
?>

使用方式

  • 於程式最上方加入: start_session(600); // 代表 600 秒後會過期 (取代原本 session_start())

如果要再延長過期時間, 只要再做修改即可.

但是有個問題要注意, 就是 PHP 的 session 預設是存成 file, 所以 /tmp 可能會因這樣設定而爆掉(檔案太多), 通常解法是把 session 存進 DB/memcache 中.

十月 6, 2008
» PHP 判斷網址是否正確 / 網頁是否存在

PHP 要判斷網頁是否存在, 簡單的方法就是 fopen / file_get_contents .. 等等, 有一堆的方式可以做, 不過這些方式都會把整頁 HTML 拉回來, 要判斷的網址資料很多時, 就會有點慢.

要判斷可以由 HTTP HEADER 來判斷, 就不用把整頁的內容都抓回來(詳可見: Hypertext Transfer Protocol -- HTTP/1.1).

fsockopen 判斷 HTTP Header

簡單的範例如下(轉載自: PHP Server Side Scripting - Checking if page exists)

if ($sock = fsockopen('something.net', 80))
{
   fputs($sock, "HEAD /something.html HTTP/1.0\r\n\r\n");

   while(!feof($sock)) {
       echo fgets($sock);
    }
}

會得到下述資料:

HTTP/1.1 200 OK
Date: Mon, 06 Oct 2008 15:45:27 GMT
Server: Apache/2.2.9
X-Powered-By: PHP/5.2.6-4
Set-Cookie: PHPSESSID=4e037868a4619d6b4d8c52d0d5c59035; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Vary: Accept-Encoding
Connection: close
Content-Type: text/html

但是上述做法, 還是會有很多問題, 例如 302 redirect 等等, 簡單點的方法, 還是靠 curl 來幫我們處理掉這些麻煩事吧~

PHP + Curl + Content-Type 判斷

PHP + Curl 判斷此網頁是否存在, 詳可見: How To Check If Page Exists With CURL | W-Shadow.com

此程式會判斷 200 OK 等狀態資訊(200 ~ 400 間都是正常的狀態).

基本上, 上述那程式已經夠用, 不過使用者輸入的資料是千奇百怪的, 所以需要加上其它的判斷, 下述是隨便抓幾個有問題的網址:

  • xxx@ooo.com # Email
  • http://xxx.ooo.com/abc.zip # 壓縮檔
  • <script>alert('x')</script> # 幫你檢查是否有 XSS 漏洞 =.=|||

因為上述資料, 所以要把上述資訊 Filter 掉, 所以要多檢查是否是正常網址, 和 Content-Type 是否是我們要的.

於是程式修改如下(修改自: How To Check If Page Exists With CURL):

<?php
function page_exists($url)
{
   $parts = parse_url($url);
   if (!$parts) {
      return false; /* the URL was seriously wrong */
   }

   if (isset($parts['user'])) {
      return false; /* user@gmail.com */
   }

   $ch = curl_init();
   curl_setopt($ch, CURLOPT_URL, $url);

   /* set the user agent - might help, doesn't hurt */
   //curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
   curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; wowTreebot/1.0; +http://wowtree.com)');
   curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);

   /* try to follow redirects */
   curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

   /* timeout after the specified number of seconds. assuming that this script runs
      on a server, 20 seconds should be plenty of time to verify a valid URL.  */
   curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
   curl_setopt($ch, CURLOPT_TIMEOUT, 20);

   /* don't download the page, just the header (much faster in this case) */
   curl_setopt($ch, CURLOPT_NOBODY, true);
   curl_setopt($ch, CURLOPT_HEADER, true);

   /* handle HTTPS links */
   if ($parts['scheme'] == 'https') {
      curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  1);
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
   }

   $response = curl_exec($ch);
   curl_close($ch);

   /* allow content-type list */
   $content_type = false;
   if (preg_match('/Content-Type: (.+\/.+?)/i', $response, $matches)) {
       switch ($matches[1])
        {
           case 'application/atom+xml':
           case 'application/rdf+xml':
           //case 'application/x-sh':
           case 'application/xhtml+xml':
           case 'application/xml':
           case 'application/xml-dtd':
           case 'application/xml-external-parsed-entity':
           //case 'application/pdf':
           //case 'application/x-shockwave-flash':
              $content_type = true;
              break;
        }

       if (!$content_type && (preg_match('/text\/.*/', $matches[1]) || preg_match('/image\/.*/', $matches[1]))) {
           $content_type = true;
        }
   }

   if (!$content_type) {
      return false;
   }

   /*  get the status code from HTTP headers */
   if (preg_match('/HTTP\/1\.\d+\s+(\d+)/', $response, $matches)) {
      $code = intval($matches[1]);
   } else {
      return false;
   }

   /* see if code indicates success */
   return (($code >= 200) && ($code < 400));
}

// Test & 使用方法:
// var_dump(page_exists('http://tw.yahoo.com'));
?>

Content-Type information

上述 Content-Type 的資訊可由下述找到:

  • /etc/mime.types
  • /usr/share/doc/apache-common/examples/mime.types.gz
  • /usr/share/doc/apache2.2-common/examples/apache2/mime.types.gz # 建議是看這個

相關網頁

九月 25, 2008
» UUID generator function (PHP/Javascript/Ruby)

要產生 唯一值 的做法, 在 PHP 可以用 unique().

除了用 unique 外, 要產生唯一來辨視用的值, 可以考慮使用 UUID(Universally Unique Identifier).

UUID 目的

取自 UUID 維基百科

UUID 的目的,是讓分散式系統中的所有元素,都能有唯一的辨識資訊,而不需要透過中央控制端來做辨識資訊的指定。

UUID 文件

  • 詳見: RFC 4122 (A Universally Unique IDentifier (UUID) URN Namespace)

PHP UUID 程式

Javascript UUId 程式

Ruby UUID

UUID 產生器

相關網頁

其它相關

PHP 產生唯一檔名的檔案, 可以使用: Generate unique filenames in PHP, 或者使用 PHP 內建的 tempnam function(Create file with unique file name).

範例: echo tempnam('./','cookie'); // 這樣就會在此目錄產生 cookie 開頭檔名的檔案

九月 24, 2008
» PHP 判斷 Header 送出前, 是否有值被送出去: headers_sent()

header() 最常被拿來送 header('Location: /'); 等等, 做網頁導向的動作.

除了這些動作外, 還有 setcookie(), header()... 等, 這些 function 在執行前, 頁面上都不能有任何輸出(空白也不行), 若有任何輸出, 就會有下述的錯誤訊息:

<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /var/www/test.php:5) in <b>/var/www/test.php</b> on line <b>6</b><br />

要預防這種錯誤發生, 可以使用 headers_sent() 判斷, 若之前已經有輸出, 就寫個 Log 檔會其它處理, 不要讓錯誤訊息直接秀在頁面上~:)

headers_sent() 範例

<?php
// 如果之前沒有任何輸出, 就送出 Location 的導向資訊
if (!headers_sent()) {
    header('Location: b.php');
    exit;
}
?>

另外要秀出 預計要送哪些 header 的資訊出去, 可以使用 headers_list() 查看.

headers_list() 範例

<?php
setcookie('abc', 'test');
header('Content-type: text/plain');

// headers_list() 會將預計要送的 header data 轉成 Array 回傳
print_r(headers_list());
/*
Array
(
    [0] => X-Powered-By: PHP/5.2.6-3
    [1] => Set-Cookie: abc=test
    [2] => Content-type: text/plain
)
*/
?>

九月 23, 2008
» Linux Bash 命令: Join (指定某個欄位為主, 來合併多個檔案)

常會遇到有幾個文字檔, 裡面有幾個欄位是跟另外檔案是一致的, 要把他合併成一個檔案, 這時後最快的就是用 join.

  • Man 說明: join - join lines of two files on a common field

這次遇到的狀況是:

  1. find . -size 0 > a # 找出 size 是 0 的 "圖檔檔名"
  2. b # 從 DB 撈出所有的 "相對應的帳號 => 圖檔檔名"
  3. 要將 size = 0 的圖檔都抓出來, 再將 a 和 b 做 join, 會把 a 和 b 依照 "圖檔檔名" 做結合, 就可知道哪些帳號的圖檔 Size = 0, 再來做其它處理囉~
  4. 註: 要注意要結合的那個欄位, 要先做過 sort, 不然可能會有某些欄位無法 match.
  5. 最後再 find . -size 0 -delete 全部砍掉~ :P

依某個欄位合併兩個檔案的範例實作(Join)

  1. $ cat a
    3   aa
    5   uu
    4   bb
    7   38
    2   oo
  2. $ cat b
    uu  test
    38  abc
    oo  def
  3. $ sort -k 2 a | sed "s/   /:/" > a.sorted # 將主 key 做排序, 並將空白變成 ":"
    7:38
    3:aa
    4:bb
    2:oo
    5:uu
  4. $ sort b | sed "s/  /:/" > b.sorted # 將主 key 做排序, 並將空白變成 ":"
    38:abc
    oo:def
    uu:test
  5. join -t ":" -1 2 a.sorted -2 1 b.sorted # 或 join -t: -1 2 a.sorted -2 1 b.sorted 皆可
    38:7:abc
    oo:2:def
    uu:5:test
  6. 上述 join 就是得到合併完成的結果囉~
    • 解釋上述 join 命令的意思:
    • -t: 或 -t ":" : 依照 ":" 做切割欄位
    • -1 2 a.sorted : 第一個檔案 a.sorted, 要拿他的第二欄出來
    • -2 1 b.sorted : 第二個檔案 b.sorted, 要拿他的第一欄出來
    • 這樣子就會把這兩個檔案抽出來的欄位做 match(join), 就會是結果~ :)

九月 22, 2008
» PHP 為 Regex 加跳脫字元 的 function: quotemeta

使用 Regular expression 時, 有以下這些特殊用途的符號:

. \ + * ? [ ^ ] ( $ )

若要處理某些輸入, 直接丟給 preg_match / preg_replace.. 等 使用, 就會有出乎意料外的結果.

要避免此意外, 丟進去前, 要先將那字串做處理, quotemeta 會 對特殊用途的符號加上 跳脫字元(backslash)"\".

例如:

<?php
$str = 'ooxx...book+pen...123';
$ex = 'book+pen';
print_r(preg_replace("/$ex/", '', $str)); // ooxx...book+pen...123

$ex = quotemeta('book+pen');
print_r(preg_replace("/$ex/", '', $str)); // ooxx......123
?>

使用上述做法的主要用途在於 $ex 的變數, 是隨時做改變的. (function 的變數或是外來的變數等)

若是 固定字串, 有簡單又快速的方式處理:

<?php
$str = 'ooxx...book+pen...123';
print_r(preg_replace("/\Qbook+pen\E/", '', $str)); // ooxx......123
print_r(str_replace("book+pen", '', $str)); // ooxx......123
?>

附註:

  • Regex 中: \Q\E 包住的字串, 會將此字串直接做取代,不會理會裡面的特殊符號.
  • str_replace() 用來取代固定字串, 速度會比 preg_replace 快上 N 倍.

 

九月 15, 2008
» IE 對 CSS 相容性文件 (MSDN)

Yahoo! UI Library: Graded Browser Support 中, 在 IE 6.0 沒有從 A-grade 移除前, 還是乖乖的把文件看清楚.

IE CSS 相容性的相關文件

九月 11, 2008
» Project release 的 更新/還原 控制 Script

專案完成後, 第一次就直接 Release 即可, 那之後的版本更新應該怎麼辦?

常見的幾種做法:

  1. 開發者更新程式是直接修改線上版
  2. 開發者先在測試環境修改完程式, 再將修改過的程式複製到線上 (或直接 svn release / svn up 蓋掉線上檔案)
  3. 將舊版的 proejct_name mv 做成 舊版project_name-當天日期, 再將新版程式改成 project_name.
  4. 不管哪種版本, 總之全都加上日期(ex: prject_name-日期), 然後用 ln 的方式, 將 project_name link 到最新版的程式.
  5. 最標準的做法: 包裝成 Package, 然後去做 Release, 當有問題時, 可再馬上還原回舊版的程式.

前面兩種做法 和 包 Package 的做法, 在此就不討論:

  • 前兩種做法 都是開發者操作, 有什麼問題應該都會想辦法解決.
  • 包 Package 的做法, 只要會推 Package 即可. (當然前題是 Package 要包的好.. XD)

現在的狀況是, 希望能用 svn release, 但是若出問題時, 又想要能快速的 rollback, 不過問題是出在做這件事的人, 是一般使用者, 所以要考慮讓他能用最簡單的指令讓他完成這件事. (暫時先不考慮 包 Package 的做法)

目前預計是使用 3 和 4 的做法:(下述 Script 在 Debian/Ubuntu Linux 可正確執行, BSD 等沒測試過)

採用 將舊版的 proejct_name mv 做成 舊版 project_name-當天日期, 再將新版程式改成 project_name.

Release 流程:
  1. 將舊版的 Project-Name 改名字成 Project-Name-timestamp
  2. 然後再將新版的程式搬過去, 改名字叫 Project-Name 即可.
  3. Script(release.sh) 範例:(假設 DocumentRoot = /var/www/project_name)
    #!/bin/sh
    # 從 svn checkout release version
    svn release http://hostname/project_name
    # 將現在的版本加上 timestamp 搬到舊版本去
    mv /var/www/project_name /var/www/project_name-`date "+%s"`
    # 將 release 的版本搬到預定位置
    mv project_name /var/www/
Rollback 流程:
  1. 砍掉現在運行的版本
  2. 抓取上一版的程式, 改名成 Project-Name 即可.
  3. Script(rollback.sh) 範例:
    #!/bin/sh
    # 砍掉現在版本
    rm -fr /var/www/project_name
    # 取出上一版的程式
    ROLLBACK_VERSION=`ls /var/www | sort | tail -1`
    echo "use this version: $ROLLBACK_VERSION"
    # 將上一版的程式搬回去, 還原當最新版本
    mv /var/www/$ROLLBACK_VERSION /var/www/project_name

但是這種做法有個缺點, 刪除/搬移 時或許會有個 1~2秒的時間會有問題, 特別是當你的原始碼檔案越多時, 刪除所需時間就會越久.

由於上述缺點, 所以來想改善措施, 所以就採用第 4 種的方法, 如下述:

不管哪種版本, 總之全都加上日期(ex: prject_name-日期), 然後用 ln 的方式, 將 project_name link 到最新版的程式.

Release 流程
  1. 將 svn release 的目錄名稱, 改為 Project-Name-timestamp
  2. 再用 ln 去抓取目前最新版的 Project-Name-timestamp, 把他指到 project_name 去.(project_name = Apache 指定的 DocumentRoot)
  3. Script(release-ln.sh) 範例:
    #!/bin/sh
    # 從 svn checkout release version, 目錄產生為 project_name + timestamp
    svn release http://hostname/project_name project_name
    # 現在的 timestamp
    TIMESTAMP=`date "+%s"`
    # 將 release 的版本加上 timestamp 搬到網頁存放目錄去.
    mv project_name /var/www/project_name-$TIMESTAMP
    # 將 ln 指到新的網頁存放目錄
    ln -fs /var/www/project_name-$TIMESTAMP /var/www/project_name
Rollback 流程
  1. 將現在運行的程式 ln 指到上一版的程式目錄
  2. 再移除最新版的程式, 這樣子就達到還原為上一版的程式囉~
  3. Script(rollback-ln.sh) 範例:
    #!/bin/sh
    # 抓上一版的程式版本
    PRE_VERSION=`ls /var/www | sort | tail -2 | head -1`
    # 現在這版的程式版本
    NOW_VERSION=`ls /var/www | sort | tail -1`

    # 如果只剩一個版本, 不能移除
    if [ "$PRE_VERSION" == "$NOW_VERSION" ]; then
        exit
    fi

    # ln 將上一版的指到現在該執行的網頁存放目錄
    ln -fs /var/www/project_name-$PRE_VERSION /var/www/project
    # 砍掉現在版本
    rm -fr /var/www/project_name-$NOW_VERSION
    echo "use this version: $PRE_VERSION"

這種在速度上, Release/Rollback 上都很方便, 缺點是目錄會留下不少, 可能每年要記得去清清過期已久的程式.

使用方式

  1. 使用者 ssh 登入主機後
  2. ./release.sh # Release
  3. 若有發生異常, 要還原: ./rollback.sh # 還原回上一版.
  4. 這樣子就可以囉.

註: 某公司的 Package release 也是採用類似 ln 的做法. :P

九月 10, 2008
» PHP 分割字串 Function 的速度比較(substr/sscanf/preg_match)

固定長度的字串(假設是 06481a63041b578d702f159f520847f8), 要照固定格式做切割, 使用 PHP 要怎麼切會比較快?

註: 要將此字串切成 => 06 / 48 / 1a63041b578d702f159f520847f8 這三個字串.

寫簡單的程式做個測試, 來比較 substr / sscanf / preg_match 的速度.

先設 $a = '06481a63041b578d702f159f520847f8';, 再執行下面程式做測試(全部都跑 100萬次的數據):

  • 使用 substr 程式執行花 4.08482551575 秒:
    $x[0] = substr($a, 0, 2);
    $x[1] = substr($a, 2, 2);
    $x[2] = substr($a, 4, 28);
  • 使用 sscanf 程式執行花 5.26213645935 秒 (與 substr 差 -5.26213645935)
    list($x[0], $x[1], $x[2]) = sscanf($a, '%2s%2s%28s');
    註: $x = sscanf($a, '%2s%2s%28s'); // 耗費時間增加到 8秒, 會比 preg_match 更慢.
  • 使用 preg_match 程式執行花 7.58504867554 秒 (與 substr 差: -7.58504867554)
    preg_match('/^(\w{2})(\w{2})(\w{28})$/', $a, $match);
    $x[0] = $match[1];
    $x[1] = $match[2];
    $x[2] = $match[3];

結論, substr() 大獲全勝, 還是不要懶惰, 雖然要寫比較多行, 但是速度會快不少.

九月 8, 2008
» PHP: 使用 range() 快速產生序列資料

寫 Perl 都喜歡用 1..10, 就會自動產生 1~10 的數字, 但是在 PHP 要怎麼做呢?

Perl example:

for (1..10) {
    print $_;
}

PHP 要快速產生 1~10 的 Array 可以用 range(), 使用範例如下(範例參考自 PHP range 文件):

// 產生數字序列, array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
foreach (range(1, 10) as $n) {
    echo $n;
}

// 產生英文字序列, array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i');
foreach (range('a', 'i') as $n) {
    echo $n;
}

// 產生0~100 的數字, 且數字間差距為 10 的數字序列, array(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
foreach (range(0, 100, 10) as $n) {
    echo $n;
}

八月 31, 2008