對於學分保存方案,我們想能夠檢索任何給定的測驗或測試的分數。
對於歷史聯盟,我們想做下面的事情:
以不同格式產生成員目錄。我們想在年度宴會程序中,以可以用於生成顯示目錄的格式使用一個只有名稱的列表。
尋找不久就要更新其成員資格的League 成員,然後發送電子郵件通知他們。
編輯成員項目(畢竟,在更新成員資格時,我們將要更新他們的終止日期)。
尋找分享共同興趣的成員。
使這個目錄聯機。
對於這樣一些任務,我們將編寫從命令行上運行的腳本。其他任務,我們將在7 . 4節“在Web 應用程序中使用DBI”中創建腳本,可以與Web 服務器配合使用。在本章的最後,我們將仍有許多有待完成的目標。將在第8章“PHP API”中,完成剩余的目標。
生成歷史同盟目錄
我們的目標之一是能以不同格式產生歷史同盟目錄的信息。我們將生成的最簡單格式是一個年度宴會程序的成員名列表。那可能是一個簡單的無格式文本列表。它將成為創建這個程序的一部分較大文檔,所以,我們所需要的就是可以粘貼到文檔中的一些內容。
對於可顯示的目錄,則需要一種比無格式文本更好的表示方法,原因是我們想把一些內容更精細地格式化。這裡一個合理的選擇為RT F(豐富的文本格式Rich Text Format),它是由Microsoft 開發的一種格式,可以由許多字處理程序來識別。當然, Word 就是這種程序之一,但是許多其他的軟件,如WordPerfect 和A p p l e Work 也是可以識別的。不同的字處理程序對RTF 的支持程度也有所不同,但是我們將使用由即使對最低級別RT F都確信的任何字處理程序所支持的全部RTF 規定的一個基本子集。
生成宴會列表和RTF 目錄格式的過程本質上是一樣的:發布查詢來檢索這些項目,然後運行將每個項目提取和格式化的循環。給出了基本的相似之處,就能很好地避免編寫兩個分開的腳本。所以,我們編寫一個單獨的腳本g e n _ d i r,它可以以不同的格式從這個目錄生成輸出。我們可以這樣組織這個腳本:
1) 在編寫出項目內容之前,完成這個輸出格式可能需要的任何初始化。宴會程序成員列表不需要任何特殊的初始化,但是我們需要為這個RTF 版本編寫一些初始的控制語言。
2) 提取和顯示每個項目,將我們要輸出的類型適當地格式化。
3) 處理完所有的項目之後,還要完成任何必需的清除和終止。除了這個RTF 版本需要的一些關閉控制語言以外,宴會列表不需要特殊的處理。
將來,我們可能想使用這個腳本以其他格式編寫輸出,所以我們通過設置“轉換盒”——每個輸出格式都有一個元素的散列,使它成為可擴展的。每個元素都指定對給定格式生成適當輸出的函數:初始化函數、編寫項目函數和清除函數如下所示:
由一個格式名(在這種情況下的“ b a n q ue t”和“r t f”)標識轉換盒的每個元素。我們將編寫這個腳本,以便在運行它時可以在命令行中指定想要的格式:
% gen_dir banquet
% gen_dir rtf
通過以這種方式設置轉換盒,我們可以很容易地增加新格式的性能:
1) 編寫三個格式化函數。
2) 向轉換盒增加一個指向那些函數的新元素。
3) 為了以新的格式產生輸出,調用g e n _ d i r,並在命令行中指定這個格式名。
按照命令行中的第一個參數所選擇的適當轉換盒項目的代碼如下所示。它是由於輸出格式的名稱為%switchbox 散列中關鍵字。如果在轉換盒中不存在這樣的關鍵字,則這個格式是無效的。不需要這個代碼中的硬連線格式;如果向轉換盒增加新的格式,則自動地檢測它。如果在命令行中沒有指定格式名,或者指定了一個無效的名稱,則這個腳本產生錯誤消息,並顯示一列允許的名稱:
如果在命令行指定了一個有效的格式名,則前述的代碼設置$ f un c _ h a s h r e f。它的值將是指向選擇了格式輸出的編寫函數的散列引用。然後我們可以運行這個選擇項目的查詢。之後,我們調用初始化函數、提取和顯示這些項目,並激活清除函數:
因為某種原因,提取項目的循環使用了fetchrow_hashref( )。如果這個循環提取數組,則這個格式化函數必須知道列的次序。它可能通過訪問$sth->{NAME} 屬性(它含有返回次序的列名)來得到,但為什麼煩擾呢?通過使用散列引用,格式化函數將只能命名那些想使用$entry_ref->{col_name} 的列值。那樣效率就非常低,但它容易做到,並可用於想生成的任何格式,因為我們知道我們需要的任何域都在散列中。
剩余的工作就是為每種輸出格式編寫這些函數(也就是說,通過轉換盒項目為這些函數命名)。
1. 生成宴會程序成員列表
對於這種輸出格式,我們只想要成員的姓名。不需要初始化或清除調用。只需要一個項目格式化函數:
format_banquet_entry( ) 的參數是行的列值的散列引用。這個函數將名和姓連在一起,加上可能出現的任何後綴。這裡的竅門是如“ J r.”或“S r.”後綴的前面應該有一個逗號或空格,但是如“I I”或“I I I”後綴的前面只能為一個空格:
因為字母‘I’、‘V’和‘X’覆蓋了所有生成的數字,從第1到第3 9,所以我們可以使用下面的測試來確定是否增加一個逗號:
和名稱放在一起的format_banquet_entry( ) 的代碼也是這個目錄的RTF 版本將需要的一些內容。然而,並不是復制format_rtf_entry( ) 中的代碼,讓我們將它填入函數中:
將確定名稱的字符串放在format_name( ) 函數中,將把format_banquet_entry( ) 函數減少到幾乎沒有:
2. 生成顯示格式的目錄
生成這個目錄的RTF 版本比生成宴會程序成員列表更要棘手一些。首先,我們需要從每個項目中顯示更多的信息。其次,我們需要用每個項目產生一些RTF 控制語言來完成我們想要的作用。RTF 文檔的最小框架是這樣的:
這個文檔用花括號‘ {’和‘ }’作為開始和結束。RTF 關鍵字用反斜線符號開始,並且文檔的第一個關鍵字必須為 r t f n,n為這個文檔對應的RTF 規定的版本號。如果按我們的目的,0就比較合適。
在這個文檔的內部,我們指定字體表來說明這些項目所使用的字體。字體表信息列在組中,由含有前導的fonttbl 關鍵字和一些字體信息的花括號組成。在框架中說明的這個字體表把字體號0 定義為Ti m e s(我們只需要一個字體,但是如果想顯示得更好一些,可以使用多種字體)。
下面的一些指示設置了缺省格式風格: plain 選擇無格式的格式, f0 選擇字體0(我們已經在字體表中定義為Times ),fs24 設置字體大小為12個點陣(fs 後面的數量表示半個點陣的大小)。設置頁邊空白並不是必需的;大多數的字處理程序將提供合理的缺省值。
要想得到一個非常簡單的方法,可以將每個項目顯示為一系列的行,每行上都有一個標號。如果對應於特定輸出行的信息缺失,則忽略這個行(例如,沒有電子郵件地址的成員沒有顯示“ E m a i l :”行)。一些行(如“ A d d r e s s :”行)由多個列(街道、城市、州、郵政編碼)中的信息構成,所以這個腳本必須能夠處理缺失值的各種組合。這裡是我們將使用的輸出格式的樣例:
對於顯示的格式化項目,RTF 的表示方法如下所示:
要想使“Name :”行為粗體,則在它的前面加 (後面有個空格)來打開粗體,並用 b 0來關閉粗體。每行在末端都有一個段標記符( p a r)來告訴字處理程序移到下一行——沒有太復雜的事情。
初始化函數產生前導RTF 控制語言(請注意,兩個反斜線符號獲得輸出中的一個反斜線符號):
類似地,清除函數產生終止控制語言(並不太多!):
sub rtf_cleanup
{
print '} ";
}
真正的工作與格式化這個項目有關,即使這個任務相對簡單。主要復雜點是將地址字符串格式化,並確定應該顯示哪個輸出行:
當然,不用限於這種特殊的格式化風格。可以更改如何顯示任何域的方法,所以通過簡單地更改format_rtf_entry( ),可以幾乎任意地更改顯示的目錄。用它原始格式的目錄(一個字處理文檔),是多麼不容易做的事情!
gen_dir 腳本現在完成了。通過運行以下這些命令,我們可以以任意一種輸出格式生成這個目錄:
% gen_dir banquet > name.txt
% gen_dir rtf > directory.rtf
在Windows 中,我可以運行g e n _ d i r,則這些文件准備從基於Windows 字處理程序的內部使用。在UNIX 中,我可就以運行上面那些命令,然後將這些輸出文件以郵件形式發給自己作為附件,以便可以從我的Macintosh 中獲取它們,並將它們加載到字處理程序中。我偶爾使用mutt 郵寄程序,它允許使用-a 選項從命令行指定附件。可以如下發送給自己一個具有這兩個附加文件的消息:
% mutt -a name.txt -a directory.rtf paul0snake.Net
其他郵寄程序可能也允許創建附件。或者,可以以其他意思傳輸這些文件,如F T P。無論如何,在這些文件被放到想放的地方之後,讀取這個名稱列表,並將它粘貼到年度程序文檔,或者在可識別RTF 的任何字處理程序中讀取RTF 文件,這都是較容易的。DBI 使我們從MySQL中抽取想要的信息很容易, Perl 的文本處理能力使我們將這些信息放在指定的格式中很容易。MySQL不提供信息輸出的任何特殊方式,但沒有關系,因為將MySQL的數據庫處理能力集成到如Perl 的語言中並不費力,而這些語言具有極好的文本處理能力。
發送成員資格更新通知
當作為字處理文檔維護歷史同盟目錄時,確定需要通知哪個成員其成員資格應該更新,這是件耗費時間並且容易出現錯誤的事情。既然我們在數據庫中有信息,那麼讓我們看看如何自動地處理更新通知。我們想標識需要經過電子郵件更新的成員,這樣我們就不必通過電
話或郵件與他們聯系了。
我們需要做的事情就是確定哪個成員在某些天以內快到更新的時間了。這個的查詢涉及一個相對簡單的日期計算:
SELECT ... FROM member
WHERE expiration < DATE_ADD (CURRENT_DATE,INTERVAL cutoff DAY)
c ut o ff 表示我們同意的可允許誤差的天數。這個查詢選擇在幾天之內快到更新時間的成員項目。作為特殊情況,終止點值為0,尋找終止日期已過的成員(也就是說,實際上已經終止了的那些成員)。
我們標識了限制通知的這些記錄之後,我們對它們應該怎麼辦呢?一個選擇是直接從同樣的腳本中發送郵件,但是,首先審閱不發送任何消息的列表可能有用。由於這個原因,我們將使用一個兩階段的方法:
階段1:運行腳本need_renewal 來標識需要更新的成員。可檢查這個列表,或者可以使用它作為將更新通知發送到第2 階段的輸入。
階段2:運行腳本r e n e w a l _ n o t i f y,它通過電子郵件向成員發送“請更新”的通知。這個腳本應該通知您不具有電子郵件地址的成員,以便可以用其他方式與他們聯系。
在此任務的第一部分中, need_renewal 腳本必須標識哪個成員需要更新。它的操作如下所示:
need_renewal 腳本的輸出如下所示(因為是針對當前日期確定的結果,而您讀這本書的時間和我書寫它的時間將是不同的,所以將獲得不同的輸出)
可以觀察到,處於負數天數的那些成員資格需要更新。負數意味著我們已經過期了(當手工地維護記錄時,就可能發生這種情況;有些人從縫隙中滑掉了。既然我們在數據庫中有了這些信息,那麼我們要尋找在前面丟失的幾個人)!
更新通知任務的第二部分涉及了通過電子郵件發送通知的腳本r e n e w a l _ n o t i f y。要想使renewal_notify 更容易使用,則我們可以使它支持三類命令行參數:成員關系ID 號碼,電子郵件地址和文件名。數值的參數表示成員資格ID 值,帶有字符‘@’的參數表示電子郵件的地址。其他任何事情都解釋為應該讀取的文件名,以便找到他們的ID 號碼或電子郵件地址,可以直接在命令行中這樣做,或者通過將它們在文件中列出來去做(特別是,可以使用need_renewal 的輸出作為renewal_notify 的輸入)。對於要發送通知的每個成員,此腳本查找相應的member 表項目,抽取電子郵件地址,並向那個地址發送一條消息。如果此項中沒有電子郵件地址,則renewal_notify 生成一條消息,通知您需要以一些其他方式與這些成員聯系。
要想發送電子郵件, renewal_notify 打開與sendmail 程序的管道,並將這封郵件推入此管道中(在Windows 下不能這樣操作,Windows 中沒有s e n d m a i l。可能需要尋找發送郵件的模塊來代替它使用)。在此腳本開頭附近,將到sendmail 的路徑名設置為參數。可能需要更改該路徑,因為sendmail 的位置隨系統的變化而變化:
# change path to match your system
my ($sendmail)="/usr/lib/sendmail -t -oi";
主要參數處理循環的操作如下所示。如果在命令行沒有指定參數,則我們讀取標准的輸出作為輸入。否則,我們通過將參數傳遞給i n ter p r e t _ a rgument( ),將它分類為ID 號、電子郵件地址或者文件名來處理每個參數:
函數read_file( ) 讀取了文件的內容(假設已經打開),並查看每行的第一個域(如果我們將need_renewal 的輸出作為renewal_notify 的輸入,則每行都有若干域,但是我們只想查看第一個域)。
i n ter p r e t _ a rgument( ) 函數將每個參數分類,以便確定它是ID 號碼、電子郵件地址還是文件名。對於ID 號碼和電子郵件地址,它查找適當的成員項目,並將它傳遞給n o t i f y _ member ()。我們必須注意由電子郵件所指定的成員。兩個成員具有同樣的地址是可能的(例如,丈夫和妻子),並且我們不想將一條消息發送給不能用這條消息的人。為了避免這一點,我們查找了與電子郵件地址相對應的成員的ID 號碼,來確保內容的正確。如果此地址和一個以上的ID 號碼匹配,則它是不確定的,我們在顯示一條警告消息後忽略它。
如果參數看起來不像ID號碼或電子郵件地址,則將它作為文件名讀取為進一步的輸入。在這裡,我們也必須小心——為了避免無窮循環的可能性,如果我們已經讀取一個文件,則我們不想再讀取文件:
實際上,發送更新通知的notify_member( ) 函數的代碼如下所示。如果得出這個成員沒有電子郵件地址,則什麼也不做,但是notify_member( ) 顯示一條警告消息,以便知道需要以其他某種方式與該成員聯系。可以調用具有這條消息中所顯示的這個成員資格ID 號碼的s h o w _ member,來查看全部項目—例如,找出這個成員的電話號碼和通信地址。
用它可能獲得更好的內容—例如,通過向member 表中增加一列來記錄最近更新的提示是何時發送出去的。這樣做將有助於避免過於頻繁地發送通知。實際上,我們只需假設不存在大約每月運行一次以上的程序。
現在運行這兩個腳本,從而可以這樣使用它們:
% need_renewal > junk
% (看一看junk,檢查它是否合理)
% renewal_notify junk
要想通知單個的成員,可以通過ID 號碼或電子郵件地址指定它們:
% need_renewl 18 [email protected]
歷史同盟成員項目編輯
我們開始發送更新通知之後,假設我們通知的一些人將更新他們的成員資格是個安全的措施。當這種情況發生時,我們將需要一種更新其所具有的新的終止日期項的方法。下一章中,我們將開發一種方法,在Web 浏覽器上編輯成員記錄,但是在這裡,我們將建立一個命
令行腳本e d i t _ member,允許用提示項的各部分新值的方法來更新項目。其操作如下:
如果在命令行上無參數調用,則edit_member 假設您想輸入一個新的號碼,提示放在成員項目中的初始信息,並創建新的項目。
如果在命令行上調用時帶有成員ID 號碼,則edit_member 查找這個項目的已有內容,然後提示更新每一列。如果輸入一列的值,則其替換當前的值。如果按Enter 鍵,這列並不更改(如果不知道成員的ID 號碼,可以運行show_member last_name 來查找其內容)。
如果只想更新成員的終止日期,則允許編輯全部項目的這種方式可能是不必要的過度行動。另一方面,類似這樣的腳本也提供了一種簡單的通用目的方式,來更新一個項目的任何部分而不必了解SQL 的任何知識(一種特殊的情況為edit_member 不允許更改member_id 域,因為當創建一個項目時,自動地分配這個域,並且在以後不能更改)。
edit_member 需要了解的第一件事為member 表中這些列的名稱:
然後我們可以輸入主體循環:
創建新成員項目的代碼如下所示。它請求每個member 表列,然後發布一條INSERT 語句以增加一條新記錄:
new_member( )所用的提示例程如下所示:
col_prompt( ) 帶有$show_current 參數的原因是,當這個腳本用於更新項目時,我們也對已有成員項目請求的列值使用這個函數。當創建新的項目時, $show_current 將為0,因為當前沒有值可以顯示。在編輯一個已有項目時,它將為非零。後一種情況中的提示將顯示當前的值,用戶可以簡單地通過按Enter 鍵來接受。
編輯已有成員的代碼類似於創建新成員的代碼。然而,我們有一個可操作的項目,所以提示例程顯示當前項目的值,並且edit_member( ) 函數發布一條UPDATE 語句,而不是INSERT語句:
edit_member 的問題為它不進行任何輸入值校驗。對於member 表中的大多數域,都沒有什麼校驗——它們只是字符串域。但是對於expiration 列,實際上應該檢查輸入值,以便確保它們看起來像日期。在一般目標的數據輸入應用程序中,可能想抽取有關表的信息,以便確定它的所有列的類型。然後,可能按照那些類型上的約束條件來校驗。那就比我在這裡想探求的內容涉及得更多,所以我只在col_prompt( ) 函數中增加一個快速方法,以便如果列名為“e x p i r a t i o n”,則檢查輸入的格式。最低限度的日期值檢查可以這樣來做:
這個模板測試了非數字字符分隔的三個序列的數字。這只是檢查的一部分,因為它沒有偵測如“ 1999 - 14 - 2 2”的值為無效。要想使腳本更好,則應該給它更嚴格的日期檢查以及其他檢查,如需要名和姓的域,就應該給非空值。
一些其他的改進可能是,如果沒有更改列,則跳過這個更新,當用戶正在編輯它時,如果其他一些人已經更改了這條記錄,則通知這個用戶。可以通過保存成員項目列的原始數據來做到這一點,然後,編寫UPDATE語句來只更新那些已經更改的列。如果沒有,則甚至不需要發布這條語句。同樣,對於每個原始列值,可以編寫WHERE 子句來包括A N D col_name = col_val。如果其他一些人已經更改了這條記錄,則這可能導致UPDATE失敗,此時它的反饋為,兩個人要同時更改這個項目。
尋找共同興趣的歷史同盟成員
歷史同盟秘書的責任之一就是處理成員的請求,這些成員可能要求對美國歷史領域內特殊時期或特殊人物(如在大蕭條中或者亞伯拉罕·林肯的生命)感興趣的其他人清單。當在字處理程序文檔中維護這個目錄時,使用字處理程序的“ F i n d”功能,可以非常容易地找到這樣的成員。然而,產生一列只含有合格成員的項就要困難一些,因為它涉及大量的拷貝和粘貼。使用MySQL,工作就變得容易得多,因為我們可以只運行如下這樣的查詢:
SELECT * FROM member WHERE interests LIKE "%lincoln%"
ORDER BY last_name,first_name
不幸的是,如果在MySQL客戶機程序運行這個查詢,則結果看上去並不是非常好。讓我們把少量的DBI 腳本和生成較漂亮的輸出的interests 放在一起。首先,檢查一下腳本,確保在命令行至少有一個命名的參數,因為如果沒有一個命名的參數就沒有內容可以搜索。然後,對於每個參數,腳本在member 表的interests 列上運行一個查詢:
為了搜索關鍵字字符串,我們在每一邊都放了通配符‘ %’,以便可以在interests 列的任何地方都可以找到這個字符串。然後,我們顯示相匹配的項:
這裡沒有出現format_entry( ) 函數。它與gen_dir 腳本的函數format_rtf_entry( ) 在本質上是相同的,但format_entry( ) 函數去掉了RTF 控制字。
聯機歷史同盟目錄
在7 . 4節中,我們將開始編寫連接到MySQL服務器並抽取信息的腳本,還要編寫以Web頁面形式在客戶機的Web 浏覽器中出現的信息。那些腳本按照客戶機請求動態地生成了H T M L。在我們到達那一點之前,讓我們通過編寫生成能裝載到Web 服務器文檔樹中的靜態
HTML 文檔的DBI 代碼,開始考慮有關的H T M L。以Html 格式創建的歷史同盟目錄是最好的選擇,因為我們的目標之一就是無論如何要使目錄聯機。
一般來說,Html 文檔有點像下面這樣的結構:
為了以這種格式生成目錄,編寫完整的腳本對於你來講並不必要。回想一下,當我們編寫gen_dir 腳本時,我們使用了可擴展的框架,因此,為了以其他格式產生目錄而插入了代碼。這意味著假如代碼生成了Html 輸出,我們則需要編寫文檔初始化和清除的函數,和格式化單獨項一樣。然後我們需要創建轉換盒元素來指向這些函數。
只顯示出的聯機文檔非常容易地分解為可以由初始化函數和清除函數處理的序言和收尾部分,以及由項目格式化函數生成的中間部分。HTML 初始化函數生成級別1標題的每一部分,而清除函數生成關閉</BODY> 和</Html> 標記的部分:
一般來說,真正的工作在於格式化項目。但即使這樣也不太困難。我們可以拷貝format_rtf_entry( ) 函數,確保項目中的任何特殊字符都被編碼,並且用Html 標出的標志替換RTF 控制字:
現在我們把另一個元素加到轉換盒中,指出編寫Html 的函數,並且完成對g e n _ d i r的更正:
為了產生Html 格式的目錄,運行下面的命令並在Web 服務器的文檔樹中安裝結果輸出文件:
% gen_dir html > directory.Html
當更新目錄時,可以再次運行命令來更新聯機版本。另一個方案是建立周期性執行的cron 作業。那就是說,聯機目錄將被自動地更新。例如,我可能使用類似於這個的crontab 項在每天早晨4點運行g e n _ d i r:
04****/u/paul/samp_db/gen_dir>/usr/local/apache/htdocs/directory.Html
這個cron 作業所運行的用戶必須允許它們都執行位於samp_db 目錄中的腳本,並將文件編寫到Web服務器的文檔樹中。