URL重寫是什麼?
大多數動態網站的URL中都含有變數,以告知站點哪些資訊需要展示給使用者。比如像下面這個URL,會通知相關的指令碼載入編號為7的產品:
1 |
http://www.example.com/show_a_product.php?product_id=7 |
這種URL結構的問題在於它並不容易記憶。如果是在電話中也很難讀出來(令人驚訝的是有很多人通過這種方式傳遞URL)。搜尋引擎和使用者都不能從URL中得到有用的內容資訊。你沒辦法從URL看出在這個頁面能買挪威的藍鸚鵡(的羽毛)。這種是相當標準的URL——也就是一般你在CMS網站上看到的那種。相比下面的URL:
1 |
http://www.example.com/products/7/ |
這種URL更清晰,也更短,更容易記憶,也更容易被念出來。儘管如此,它還是不能告訴別人它所指向的內容是什麼。但是我們可以更進一步:
1 |
http://www.example.com/parrots/norwegian-blue/ |
這就是我們要的東西了。即使不看上下文,我們也可以從這個URL中看出你要找的東西就在這個頁面上。搜尋引擎可以將這個URL分割成單詞(搜尋引擎會將URL中的連字元當做空格,但是下劃線不是),然後根據這些資訊更好地判斷頁面內容。這種URL是易於記憶和傳遞的。
不幸的是,要讓伺服器理解最後一種URL,需要我們做一番工作。當URL發起一個請求,伺服器需要知道如何處理URL,才能知道該返回給使用者什麼內容。URL重寫就是這種將最後一種URL“翻譯”成伺服器能理解的語言的技術。
平臺和工具
根據你的伺服器上執行的軟體,你可能已經有URL重寫模組。如果沒有,大多主機都提供啟用或安裝相關模組的功能,你可以嘗試啟用它。
Apache啟用URL重寫是最簡單的,它通常帶有自己的內建URL重寫模組——mod_rewrite,啟動和使用mod_rewrite就像上傳和命名檔案一樣簡單。
IIS是微軟的伺服器軟體,標配並不包含URL重寫的能力,但是有很多外掛提供了這種功能。ISAPI_Rewrite 是我比較推薦一款外掛,這是我發現的一款功能最接近mod_rewrite的外掛。在這篇文章的最後附有ISAPI_Rewrite的安裝和配置說明。
下面的程式碼是一些使用mod_rewrite的例子。
基本的URL重寫
首先我們來看一個簡單的例子。我們有一個網站,含有一個單獨的PHP指令碼,展示了一個單獨的頁面,URL如下:
1 |
http://www.example.com/pet_care_info_07_07_2008.php |
我們想要簡化URL,理想的URL像這樣:
1 |
http://www.example.com/pet-care/ |
為了讓這個URL有效,我們需要讓伺服器在內部將所有的“pet-care”的請求重定向到“pet_care_info_07_07_2008.php”。我們希望這個工作在內部進行是因為我們不希望使用者瀏覽器的位址列發生改變。
為了達到這個目的,我們首先需要建立一個名為“.htaccess”的文字文件來儲存我們的規則。這個檔案必須命名成這樣(不能是“.htaccess.txt”或者“rules.htaccess”)。這個檔案應該放在伺服器的根目錄(本例中放在與 “pet_care_info_07_07_2008.php” 相同的目錄中)。可能那裡已經有一個.htaccess檔案了,這時我們就應該編輯這個檔案而不是覆蓋它。
.htaccess檔案是伺服器的配置檔案。如果檔案中有錯誤,伺服器會提示錯誤資訊(通常錯誤程式碼是500)。如果使用FTP協議向伺服器傳送檔案,必須使用ASCII編碼傳輸,而不是BINARY。在本例中,這個檔案有兩個作用:1. 通知Apache啟動重寫引擎; 2. 把我們的重寫規則告訴Apache。因此我們需要在這個檔案中加入以下內容:
1 |
RewriteEngine On # Turn on the rewriting engine RewriteRule ^pet-care/?$ pet_care_info_01_02_2008.php [NC,L] # Handle requests for "pet-care" |
有幾個需要注意的地方:在.htaccess檔案中,‘#’之後的文字都會被當做註釋忽略,建議大家多使用註釋;“RewriteEngine”這一行在每個.htaccess檔案中應當只使用一次(請注意在後面的程式碼中都不包括這一行)。
“RewriteRule”行是見證奇蹟的地方。這一行可以分為五個部分:
- RewriteRule —— 通知Apache這是一條獨立的RewriteRule。
- ^/pet-care/?$ —— 模式串,伺服器會檢查每一條URL是否跟這個模式串匹配。如果匹配,Apache會使用後面的Substitution塊替換URL。
- pet_care_info_01_02_2003.php —— Substitution。如果請求與前面的模式串匹配,則Apache會使用這個URL替換原來的URL。
- [NC,L] —— 標記,告訴Apache如何應用規則。在本例中,我們使用了兩個標記:“NC”告訴Apache這條規則不區分大小寫;“L”告訴Apache如果這條規則被應用,則不再應用其他規則。
- # Handle requests for “pet-care” —— 註釋,用來解釋這條規則做了什麼(可選,但是建議這樣做)
這種規則是重寫一個獨立URL的簡單方式,也是幾乎所有URL重寫的基礎。
模式和替換
前面提到的規則使你可以重定向單獨的URL,但是mod_rewrite的強大之處在於根據包涵的模式串,識別和重寫成組的URL。
現在我們要把站點上所有的URL改成像前面舉得例子中說的那樣。你現在的URL是這樣的:
1 |
http://www.example.com/show_a_product.php?product_id=7 |
你想把它改成這個樣子:
1 |
http://www.example.com/products/7/ |
我們可以只寫一條規則來管理所有產品id,而不用對每個id都寫一條規則。其實就是你想把這種型別的URL:
1 |
http://www.example.com/show_a_product.php?product_id={a number} |
改成這種樣子的:
1 |
http://www.example.com/products/{a number}/ |
為了達到這個目的,你需要使用正規表示式。正規表示式是一種格式特殊的、方便伺服器理解的pattern。一種典型的用來匹配數字的正規表示式像這樣:
1 |
[0-9]+ |
方括號內含有一系列的字元,“0-9”表示所有數字。加號表示將會匹配任何加號前出現的pattern——本例中也就是“一個或多個數字”——也就是我們要在URL中尋找的東西。
整個的模式部分一般都會被當做正規表示式進行處理——你不需要啟動或啟用它們。
1 |
RewriteRule ^products/([0-9]+)/?$ show_a_product.php?product_id=$1 [NC,L] # Handle product requests |
首先應該注意的是用括號括起來的模式串,這樣我們可以在接下來的Substitution中使用被括號中pattern匹配的URL進行“back-reference”(向後引用)。Substitution中的“$1”告訴Apache將之前被括號裡面的pattern匹配到的URL字串放到這裡。你可以有很多back-reference,他們按出現順序編號。
像上面的RewriteRule語句,將會使Apache把所有domain.com/products/{number}/的請求重定向到show_a_product.php?product_id={same number}。
正規表示式
本文要講的並不是完整的正規表示式的指南。然而,需要注意的重點是整個pattern都會被當做正規表示式處理,要一直注意正規表示式中那些特殊的字元。
最典型的例子是在pattern中使用句號。在一個patter中,’.’ 表示“任意字元”,而不是一個普通的句號,所以當你想要匹配一個句號的時候,你需要對句號進行“轉義”——就是在它前面加一個特殊的符號,反斜槓,它會讓Apache將下一個字元當做普通字元處理。
比如,下面這條語句 ,將會匹配”rss1xml”、”rss-xml”這樣的URL:
1 |
RewriteRule ^rss.xml$ rss.php [NC,L] # Change feed URL |
這樣做一般不會有什麼很嚴重的問題,但是對字元進行適當轉義的習慣對深入學習正規表示式有好處。所以最好的寫法應該是這樣:
1 |
RewriteRule ^rss\.xml$ rss.php [NC,L] # Change feed URL |
這種情況只適用於模式比配,而不能用於替換。此外還有其他的字元(我們稱之為“元字元”)用於轉義:
- . (任意字元)
- *(零個或多個字元)
- +(一個或多個字元)
- {}(匹配至少、至多個字元)
- ? (非貪婪限制符,跟在任何一個限制符後面表示該
- ! (負向預查,出現在字串頭表示從不匹配該字串的位置開始匹配)
- ^(匹配輸入字串的開始位置,出現在範圍中表示負)
- $(表示字串結尾)
- [](包含,預設是一個字元長度)
- – (當在方括號中出現時表示範圍)
- () (段域)
- | (對兩個匹配條件進行邏輯或運算)
- \(將下一個字元轉義)
通過使用正規表示式,我們可以匹配任意URL並且重寫它們。現在回到我們文章開頭提到的例子,我們希望匹配並重寫這條URL:
1 |
http://www.example.com/parrots/norwegian-blue/ |
我們想要把這條URL翻譯成如下的格式交給伺服器:
1 |
http://www.example.com/get_product_by_name.php?product_name=norwegian-blue |
我們可以用很簡單一條規則完成這個工作:
1 |
RewriteRule ^parrots/([A-Za-z0-9-]+)/?$ get_product_by_name.php?product_name=$1 [NC,L] # Process parrots |
這個規則讓我們可以提取出URL中“parrot/”之後的任意字母、數字和連字元的組合([A-Za-z0-9-])(將連字元放在字元末尾,方括號的最後,使之被當做連字元處理,而不是分隔符),並將匹配到的產品名稱替換為$1.
如果需要的話,我們也可以讓規則更普適,使得不管產品出現在哪個目錄下,都可以傳送給相同的指令碼,就像:
1 |
RewriteRule ^[A-Za-z-]+/([A-Za-z0-9-]+)/?$ get_product_by_name.php?product_name=$1 [NC,L] # Process all products |
像這樣,我們將“parrots”替換為任意字母和連字元的組合。現在這條規則可以匹配任意在parrots目錄下或任何以一個或多個字母和連字元組成的名稱的目錄下的產品了。
修正符
修正符跟在重寫規則的最後,用來告知Apache如何解釋和處理規則。比如可以告訴Apache處理規則時不區分大小寫,當遇到第一個匹配時終止匹配,或其他更多的選項。修正符用逗號隔開,並寫在方括號裡。下面是一些修正符和他們的含義(關於這些符號有一份速查表,不需要全部記住)。
- C (與下一條規則關聯)
- CO(設定特殊Cookie)
- E=var:value (設定環境變數var: value)
- F (禁用URL,返回403HTTP狀態碼)
- G (強制URL為GONE,返回410HTTP狀態碼)
- H=handler (設定handler)
- L (表明當前規則是最後一條規則,停止分析以後規則的重寫)
- N (重新從第一條規則開始執行重寫過程)
- NC (不區分大小寫)
- NE (不在輸出轉義特殊字元)
- NS (只用於不是內部子請求)
- P (強制使用代理轉發)
- PT (移交給下一個處理器– 當使用多個處理器對url進行處理的情況下使用,如mod_alias)
- R (強制外部重定向)
- R=301 (強制重定向到新URL)
- QSA (追加請求字串)
- S=x (跳過接下來x個規則)
- T=mime-type (強制指定MIME型別)
移動內容
1 |
RewriteRule ^article/?$ http://www.new-domain.com/article/ [R,NC,L] # Temporary Move |
在修正符段新增R修正符可以改變RewriteRule的工作方式。這種情況下Apache會向瀏覽器返傳送一條資訊(一個HTTP頭),告訴瀏覽器內容已經被臨時移動到了替換塊中的URL處,而不是在內部進行URL重寫。替換塊內可以是絕對的URL也可以是相對的。HTTP頭中還包含了302程式碼,說明移動是臨時的。
1 |
RewriteRule ^article/?$ http://www.new-domain.com/article/ [R=301,NC,L] # Permanent Move |
如果移動是永久的,給“R”修飾符新增“=301”欄位,Apache會告訴瀏覽器內容被永久移動。與預設的R修飾符不同的是,“R=301”也會使瀏覽器位址列顯示新的地址。
這是最常見的一種URL重寫的方法來把內容移動到新的 URL(比如在本網站中就被廣泛使用,當文章地址改變時,會將使用者帶到新的地址去)。
條件
重寫規則可以在一個或多個重寫條件下進行,而且可以串聯,藉此我們可以對一些請求只使用一部分重寫。就我個人而言,我最常把這個規則應用到子域或替代域,它可以滿足各種各樣的標準,不僅僅是URL的。舉個例子:
1 2 |
RewriteCond %{HTTP_HOST} ^addedbytes\.com [NC] RewriteRule ^(.*)$ http://www.addedbytes.com/$1 [L,R=301] |
上面這條重寫規則重定向所有請求到“www.addedbytes.com”。如果沒有這個條件,這個規則將會產生一個迴圈,所有匹配的請求都會被送回給自己。規則的目的是隻重定向URL中缺少“www”的請求,重寫條件可以幫助我們達成目的。
重寫條件和重寫規則的使用方法差不多。首先寫“RewriteCond”告訴mod_rewrite這一行定義了一個條件。接下來是TestString和測試的模式串。最後是方括號內的修正符,跟RewriteRule的寫法差不多。
TestString(條件語句的第二部分)可以表示很多不同的東西。比如在上面的例子中,你可以檢測被請求的域,可以檢測使用者使用的瀏覽器,可以檢測引用URL(通常用來防止盜鏈),檢測使用者的IP地址,或者檢測其他的東西(參考“伺服器變數”一節瞭解其工作方式)。
模式串跟RewriteRule中的差不多一樣,但是有幾個小的例外。如果模式串的開始是一個特殊的字元(在“異常”一節定義的),那麼模式串將不會被解釋成一個匹配模式。這意味著如果你想要在正規表示式中使用用”<“,”>”,或者連字元開頭的字串,你得給他們加一個反斜線用來轉義。
重寫條件後面也可以像重寫規則那樣加修正符,但是隻有兩個。“NC”跟RewriteRule一樣,告訴Apache處理條件時忽略大小寫。另一個修正符是“OR”,如果你想在有一兩條條件匹配時就應用規則,而不是全部都滿足,在第一條條件後加入“OR”修正符(只有兩個條件的情況下),這樣只需要有其中一條滿足,規則就會被應用。預設行為是在多個條件下,只有所有都滿足的情況下才能夠應用規則。
異常和特例
重寫條件有很多不同的方式進行檢測——並不需要當做正規表示式的模式串,雖然正規表示式很常用。下面是一些處理重寫條件的方法選項:
- <Pattern(是否測試串比模式串小)
- >Pattern(是否測試串比模式串大)
- =Pattern(測試串和模式串是否相等)
- -d (測試串是否是一個合法的目錄)
- -f (測試串是否是一個合法的檔案)
- -s (測試串是否是一個合法的檔案且大小不為0)
- -l (測試串檔案是否存在且是一個符號連結)
- -F (通過subrequest來檢查某檔案是否可訪問)
- -U (通過subrequest來檢查URL是否合法且可訪問)
伺服器變數
伺服器變數是在重寫條件中可以被檢測的一些專案。這使得你可以根據所有的請求引數——包括瀏覽器標識、引用URL或其他的字串——來應用適當的規則。變數格式如下:
- HTTP Headers
- HTTP_USER_AGENT
- HTTP_REFERER
- HTTP_COOKIE
- HTTP_FORWARDED
- HTTP_HOST
- HTTP_PROXY_CONNECTION
- HTTP_ACCEPT
- Connection Variables
- REMOTE_ADDR
- REMOTE_HOST
- REMOTE_USER
- REMOTE_IDENT
- REQUEST_METHOD
- SCRIPT_FILENAME
- PATH_INFO
- QUERY_STRING
- AUTH_TYPE
- Server Variables
- DOCUMENT_ROOT
- SERVER_ADMIN
- SERVER_NAME
- SERVER_ADDR
- SERVER_PORT
- SERVER_PROTOCOL
- SERVER_SOFTWARE
- Dates and Times
- TIME_YEAR
- TIME_MON
- TIME_DAY
- TIME_HOUR
- TIME_MIN
- TIME_SEC
- TIME_WDAY
- TIME
- Special Items
- API_VERSION
- THE_REQUEST
- REQUEST_URI
- REQUEST_FILENAME
- IS_SUBREQ
多規則應用
越複雜的網站,就會有越複雜的規則來管理。當規則產生衝突的時候,行為是不確定的。往往在新增一條新的規則之後,會出現一些莫名其妙的問題,比如根本不起作用。如果這條規則本身是沒有問題的,那可能是之前有一條規則匹配到了這個URL,所以這條URL根本沒有被匹配到新加入的規則。
1 2 |
RewriteRule ^([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/?$ get_product_by_name.php?category_name=$1&product_name=$2 [NC,L] # Process product requests RewriteRule ^([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/?$ get_blog_post_by_title.php?category_name=$1&post_title=$2 [NC,L] # Process blog posts |
在這個例子中,產品頁面和blog頁面有不同的模式串,但是第二條規則將不會匹配到URL,因為所有能被匹配的模式都已經被第一條規則匹配到了。
解決這個問題的方式有很多。很多CMS(包括wordpress)通過在URL中增加一個表示請求型別的串來供規則匹配,比如:
1 2 |
RewriteRule ^products/([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/?$ get_product_by_name.php?category_name=$1&product_name=$2 [NC,L] # Process product requests RewriteRule ^blog/([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/?$ get_blog_post_by_title.php?category_name=$1&post_title=$2 [NC,L] # Process blog posts |
你也可以寫一個單獨的PHP頁面來處理所有請求,它可以檢查URL的第二個部分是否能匹配上一個blog或者一個產品。我通常這樣做,雖然可能會給伺服器帶來一些額外的負擔,但是他讓URL更加簡潔。
1 |
RewriteRule ^([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/?$ get_product_or_blog_post.php?category_name=$1&item_name=$2 [NC,L] # Process product and blog requests |
還可以通過設計更精確的規則和對規則進行更合理的安排來解決這個問題。想象一個blog有兩個分類集——主題和釋出年份。
1 2 |
RewriteRule ^([A-Za-z0-9-]+)/?$ get_archives_by_topic.php?topic_name=$1 [NC,L] # Get archive by topic RewriteRule ^([A-Za-z0-9-]+)/?$ get_archives_by_year.php?year=$1 [NC,L] # Get archive by year |
上面這兩條規則會衝突。當然,年份只由四位數字組成,所以你可以吧規則寫的更精確,這樣只有在主題名稱也是四位數字的時候才會產生衝突。
1 2 |
RewriteRule ^([0-9]{4})/?$ get_archives_by_year.php?year=$1 [NC,L] # Get archive by year RewriteRule ^([A-Za-z0-9-]+)/?$ get_archives_by_topic.php?topic_name=$1 [NC,L] # Get archive by topic |
mod_rewrite
Apache的mod_rewrite模組在大多Apache託管中是標配,如果你使用共享的託管服務,你不需要做什麼配置。但是如果你是在管理自己的空間,你需要啟用mod_rewrite模組。如果你在使用Apache1,你得修改httpd.conf檔案,去掉下面這行行首的”#”
1 |
#LoadModule rewrite_module modules/mod_rewrite.so #AddModule mod_rewrite.c |
如果在類Debian發行版上用Apache2,你只需要使用一下命令並重啟Apache:
1 |
sudo a2enmod rewrite |
其他發行版或其他平臺可能不太一樣。如果上面這兩種方法都不適用於你的系統,那就去google一下吧。可能需要修改Apache2的配置檔案,把“rewrite”加入到APACHE_MODULES列表裡,或者要修改httpd.conf,實在不行就下載mod_rewrite的原始碼自己編譯安裝。這些方法都不麻煩的。
ISAPI_Rewrite
ISAPI_Rewrite是IIS上一個基於mod_rewrite的外掛,它跟mod_rewrite的功能差不多,而且還有一些高質量的ISAPI_Rewrite論壇用來交流釋疑。因為ISAPI_Rewrite是IIS上的,安裝也非常簡單。
ISAPI_Rewrite的規則預設寫在httpd.ini檔案中,錯誤日誌在httpd.parse.errors檔案中。
正斜槓
在實際中我經常被URL重寫中的正斜槓所困擾,不管在模式串中、RewriteRule的替換串中還是RewriteCond的狀態中,都會困擾我。這可能是由於我經常面對不同的URL重寫引擎,然而我仍然建議大家——當一個規則無效時,先注意一下是不是正斜槓搞的鬼。我通常在mod_rewrite規則中避免使用斜槓,但是在ISAPI_Rewrite中會使用。
示例規則
把舊的域重定向到一個新的域:
1 2 |
RewriteCond %{HTTP_HOST} old_domain\.com [NC] RewriteRule ^(.*)$ http://www.new_domain.com/$1 [L,R=301] |
重定向缺少“www”的請求(新增“www”):
1 2 |
RewriteCond %{HTTP_HOST} ^domain\.com [NC] RewriteRule ^(.*)$ http://www.domain.com/$1 [L,R=301] |
重定向所有含有“www”的網頁(去掉“www”):
1 2 |
RewriteCond %{HTTP_HOST} ^www\.domain\.com [NC] RewriteRule ^(.*)$ http://domain.com/$1 [L,R=301] |
把舊頁面重定向到新頁面:
1 |
RewriteRule ^old-url\.htm$ http://www.domain.com/new-url.htm [NC,R=301,L] |