我们的目标之一是能以不同格式产生历史同盟目录的信息。我们将生成的最简单格式是一个年度宴会程序的成员名列表。那可能是一个简单的无格式文本列表。它将成为创建这个程序的一部分较大文档,所以,我们所需要的就是可以粘贴到文档中的一些内容。 对于可显示的目录,则需要一种比无格式文本更好的表示方法,原因是我们想把一些内容更精细地格式化。这里一个合理的选择为rt f(丰富的文本格式rich text format),它是由microsoft 开发的一种格式,可以由许多字处理程序来识别。当然, word 就是这种程序之一,但是许多其他的软件,如wordperfect 和a p p l e work 也是可以识别的。不同的字处理程序对rtf 的支持程度也有所不同,但是我们将使用由即使对最低级别rt f都确信的任何字处理程序所支持的全部rtf 规定的一个基本子集。 生成宴会列表和rtf 目录格式的过程本质上是一样的:发布查询来检索这些项目,然后运行将每个项目提取和格式化的循环。给出了基本的相似之处湍芎芎玫乇苊獗嘈戳礁龇挚慕疟尽k裕颐潜嘈匆桓龅ザ赖慕疟緂 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。它的值将是指向选择了格式输出的编写函数的散列引用。然后我们可以运行这个选择项目的查询。之后,我们调用初始化函数、提取和显示这些项目,并激活清除函数:
这个文档用花括号‘ {’和‘ }’作为开始和结束。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 (后面有个空格)来打开粗体,并用/ b 0来关闭粗体。每行在末端都有一个段标记符( / p a r)来告诉字处理程序移到下一行——没有太复杂的事情。 初始化函数产生前导rtf 控制语言(请注意,两个反斜线符号获得输出中的一个反斜线符号):
类似地,清除函数产生终止控制语言(并不太多!): sub rtf_cleanup { print '}/n"; } 真正的工作与格式化这个项目有关,即使这个任务相对简单。主要复杂点是将地址字符串格式化,并确定应该显示哪个输出行:
当然,不用限于这种特殊的格式化风格。可以更改如何显示任何域的方法,所以通过简单地更改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 脚本必须标识哪个成员需要更新。它的操作如下所示:
可以观察到,处于负数天数的那些成员资格需要更新。负数意味着我们已经过期了(当手工地维护记录时,就可能发生这种情况;有些人从缝隙中滑掉了。既然我们在数据库中有了这些信息,那么我们要寻找在前面丢失的几个人)! 更新通知任务的第二部分涉及了通过电子邮件发送通知的脚本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 号、电子邮件地址或者文件名来处理每个参数:
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,来查看全部项目—例如,找出这个成员的电话号码和通信地址。
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 列上运行一个查询:
在7 . 4节中,我们将开始编写连接到mysql服务器并抽取信息的脚本,还要编写以web页面形式在客户机的web 浏览器中出现的信息。那些脚本按照客户机请求动态地生成了h t m l。在我们到达那一点之前,让我们通过编写生成能装载到web 服务器文档树中的静态 html 文档的dbi 代码,开始考虑有关的h t m l。以html 格式创建的历史同盟目录是最好的选择,因为我们的目标之一就是无论如何要使目录联机。 一般来说,html 文档有点像下面这样的结构:
现在我们把另一个元素加到转换盒中,指出编写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服务器的文档树中。